Release Candidate Version 1.6
This commit is contained in:
50
Nextcloud Cookbook iOS Client/Views/CollapsibleView.swift
Normal file
50
Nextcloud Cookbook iOS Client/Views/CollapsibleView.swift
Normal file
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// CollapsibleView.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 15.12.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct CollapsibleView<C: View, T: View>: View {
|
||||
@State var titleColor: Color = .white
|
||||
@State var isCollapsed: Bool = true
|
||||
|
||||
@State var content: () -> C
|
||||
@State var title: () -> T
|
||||
|
||||
@State private var rotationAngle: Double = -90
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
isCollapsed.toggle()
|
||||
if isCollapsed {
|
||||
rotationAngle += 90
|
||||
} else {
|
||||
rotationAngle -= 90
|
||||
}
|
||||
}
|
||||
rotationAngle = isCollapsed ? -90 : 0
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "chevron.down")
|
||||
.bold()
|
||||
.rotationEffect(Angle(degrees: rotationAngle))
|
||||
title()
|
||||
}.foregroundStyle(titleColor)
|
||||
}
|
||||
|
||||
if !isCollapsed {
|
||||
content()
|
||||
.padding(.top)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
rotationAngle = isCollapsed ? -90 : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,10 +30,9 @@ struct MainView: View {
|
||||
RecipeSearchView(viewModel: viewModel)
|
||||
} label: {
|
||||
HStack(alignment: .center) {
|
||||
Image(systemName: "book.closed.fill")
|
||||
Text("All")
|
||||
.font(.system(size: 20, weight: .light, design: .serif))
|
||||
.italic()
|
||||
Image(systemName: "magnifyingglass")
|
||||
Text("Search")
|
||||
.font(.system(size: 20, weight: .medium, design: .default))
|
||||
}
|
||||
.padding(7)
|
||||
}
|
||||
@@ -43,10 +42,23 @@ struct MainView: View {
|
||||
if category.recipe_count != 0 {
|
||||
NavigationLink(value: category) {
|
||||
HStack(alignment: .center) {
|
||||
Image(systemName: "book.closed.fill")
|
||||
if selectedCategory != nil && category.name == selectedCategory!.name {
|
||||
Image(systemName: "book")
|
||||
} else {
|
||||
Image(systemName: "book.closed.fill")
|
||||
}
|
||||
Text(category.name == "*" ? String(localized: "Other") : category.name)
|
||||
.font(.system(size: 20, weight: .light, design: .serif))
|
||||
.italic()
|
||||
.font(.system(size: 20, weight: .medium, design: .default))
|
||||
Spacer()
|
||||
Text("\(category.recipe_count)")
|
||||
.font(.system(size: 15, weight: .bold, design: .default))
|
||||
.foregroundStyle(Color.background)
|
||||
.frame(width: 25, height: 25, alignment: .center)
|
||||
.minimumScaleFactor(0.5)
|
||||
.background {
|
||||
Circle()
|
||||
.foregroundStyle(Color.secondary)
|
||||
}
|
||||
}.padding(7)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,7 @@ struct TokenLoginView: View {
|
||||
@Binding var alertMessage: String
|
||||
@FocusState private var focusedField: Field?
|
||||
|
||||
@AppStorage("serverAddress") var serverAddress = ""
|
||||
@AppStorage("username") var userName = ""
|
||||
@AppStorage("token") var token = ""
|
||||
@AppStorage("onboarding") var onboarding = false
|
||||
@State var userSettings = UserSettings.shared
|
||||
|
||||
// TextField handling
|
||||
enum Field {
|
||||
@@ -28,14 +25,14 @@ struct TokenLoginView: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
LoginLabel(text: "Server address")
|
||||
LoginTextField(example: "e.g.: example.com", text: $serverAddress)
|
||||
LoginTextField(example: "e.g.: example.com", text: $userSettings.serverAddress)
|
||||
.focused($focusedField, equals: .server)
|
||||
.textContentType(.URL)
|
||||
.submitLabel(.next)
|
||||
.padding(.bottom)
|
||||
|
||||
LoginLabel(text: "User name")
|
||||
LoginTextField(example: "username", text: $userName)
|
||||
LoginTextField(example: "username", text: $userSettings.username)
|
||||
.focused($focusedField, equals: .username)
|
||||
.textContentType(.username)
|
||||
.submitLabel(.next)
|
||||
@@ -43,7 +40,7 @@ struct TokenLoginView: View {
|
||||
|
||||
|
||||
LoginLabel(text: "App Token")
|
||||
LoginTextField(example: "can be generated in security settings of your nextcloud", text: $token)
|
||||
LoginTextField(example: "can be generated in security settings of your nextcloud", text: $userSettings.token)
|
||||
.focused($focusedField, equals: .token)
|
||||
.textContentType(.password)
|
||||
.submitLabel(.join)
|
||||
@@ -52,7 +49,7 @@ struct TokenLoginView: View {
|
||||
Button {
|
||||
Task {
|
||||
if await loginCheck(nextcloudLogin: false) {
|
||||
onboarding = false
|
||||
userSettings.onboarding = false
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
@@ -83,11 +80,11 @@ struct TokenLoginView: View {
|
||||
}
|
||||
|
||||
func loginCheck(nextcloudLogin: Bool) async -> Bool {
|
||||
if serverAddress == "" {
|
||||
if userSettings.serverAddress == "" {
|
||||
alertMessage = "Please enter a server address!"
|
||||
showAlert = true
|
||||
return false
|
||||
} else if !nextcloudLogin && (userName == "" || token == "") {
|
||||
} else if !nextcloudLogin && (userSettings.username == "" || userSettings.token == "") {
|
||||
alertMessage = "Please enter a user name and app token!"
|
||||
showAlert = true
|
||||
return false
|
||||
@@ -104,14 +101,18 @@ struct TokenLoginView: View {
|
||||
|
||||
var (data, error): (Data?, Error?) = (nil, nil)
|
||||
do {
|
||||
let loginString = "\(userName):\(token)"
|
||||
let loginString = "\(userSettings.username):\(userSettings.token)"
|
||||
let loginData = loginString.data(using: String.Encoding.utf8)!
|
||||
let authString = loginData.base64EncodedString()
|
||||
DispatchQueue.main.async {
|
||||
userSettings.authString = authString
|
||||
}
|
||||
(data, error) = try await NetworkHandler.sendHTTPRequest(
|
||||
request,
|
||||
hostPath: "https://\(serverAddress)/index.php/apps/cookbook/api/v1/",
|
||||
hostPath: "https://\(userSettings.serverAddress)/index.php/apps/cookbook/api/v1/",
|
||||
authString: authString
|
||||
)
|
||||
|
||||
} catch {
|
||||
print("Error: ", error)
|
||||
}
|
||||
|
||||
@@ -28,42 +28,7 @@ enum V2LoginStage: LoginStage {
|
||||
}
|
||||
}
|
||||
|
||||
struct CollapsibleView<T: View>: View {
|
||||
@State var titleColor: Color = .white
|
||||
@State var content: () -> T
|
||||
@State var title: () -> Text
|
||||
|
||||
@State var isCollapsed: Bool = true
|
||||
@State var rotationAngle: Double = -90
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
isCollapsed.toggle()
|
||||
if isCollapsed {
|
||||
rotationAngle += 90
|
||||
} else {
|
||||
rotationAngle -= 90
|
||||
}
|
||||
}
|
||||
rotationAngle = isCollapsed ? -90 : 0
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "chevron.down")
|
||||
.bold()
|
||||
.rotationEffect(Angle(degrees: rotationAngle))
|
||||
title()
|
||||
}.foregroundStyle(titleColor)
|
||||
}
|
||||
|
||||
if !isCollapsed {
|
||||
content()
|
||||
.padding(.top, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct V2LoginView: View {
|
||||
@Binding var showAlert: Bool
|
||||
@@ -73,10 +38,7 @@ struct V2LoginView: View {
|
||||
@State var loginRequest: LoginV2Request? = nil
|
||||
@FocusState private var focusedField: Field?
|
||||
|
||||
@AppStorage("serverAddress") var serverAddress = ""
|
||||
@AppStorage("username") var userName = ""
|
||||
@AppStorage("token") var token = ""
|
||||
@AppStorage("onboarding") var onboarding = true
|
||||
@State var userSettings = UserSettings.shared
|
||||
|
||||
// TextField handling
|
||||
enum Field {
|
||||
@@ -90,7 +52,7 @@ struct V2LoginView: View {
|
||||
VStack(alignment: .leading) {
|
||||
LoginLabel(text: "Server address")
|
||||
.padding()
|
||||
LoginTextField(example: "e.g.: example.com", text: $serverAddress, color: loginStage == .serverAddress ? .white : .secondary)
|
||||
LoginTextField(example: "e.g.: example.com", text: $userSettings.serverAddress, color: loginStage == .serverAddress ? .white : .secondary)
|
||||
.focused($focusedField, equals: .server)
|
||||
.textContentType(.URL)
|
||||
.submitLabel(.done)
|
||||
@@ -124,7 +86,7 @@ struct V2LoginView: View {
|
||||
HStack {
|
||||
if loginStage == .login || loginStage == .validate {
|
||||
Button {
|
||||
if serverAddress == "" {
|
||||
if userSettings.serverAddress == "" {
|
||||
alertMessage = "Please enter a valid server address."
|
||||
showAlert = true
|
||||
return
|
||||
@@ -164,9 +126,14 @@ struct V2LoginView: View {
|
||||
return
|
||||
}
|
||||
print("Login successfull for user \(res.loginName)!")
|
||||
self.userName = res.loginName
|
||||
self.token = res.appPassword
|
||||
self.onboarding = false
|
||||
self.userSettings.username = res.loginName
|
||||
self.userSettings.token = res.appPassword
|
||||
let loginString = "\(userSettings.username):\(userSettings.token)"
|
||||
let loginData = loginString.data(using: String.Encoding.utf8)!
|
||||
DispatchQueue.main.async {
|
||||
userSettings.authString = loginData.base64EncodedString()
|
||||
}
|
||||
self.userSettings.onboarding = false
|
||||
}
|
||||
} label: {
|
||||
Text("Validate")
|
||||
@@ -188,7 +155,7 @@ struct V2LoginView: View {
|
||||
}
|
||||
|
||||
func sendLoginV2Request() async {
|
||||
let hostPath = "https://\(serverAddress)"
|
||||
let hostPath = "https://\(userSettings.serverAddress)"
|
||||
let headerFields: [HeaderField] = [
|
||||
//HeaderField.ocsRequest(value: true),
|
||||
//HeaderField.accept(value: .JSON)
|
||||
|
||||
@@ -21,21 +21,20 @@ struct RecipeCardView: View {
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 80, height: 80)
|
||||
.clipped()
|
||||
.clipShape(RoundedRectangle(cornerRadius: 17))
|
||||
} else {
|
||||
ZStack {
|
||||
Image(systemName: "square.text.square")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundStyle(Color.white)
|
||||
.padding(10)
|
||||
|
||||
}
|
||||
.background(Color("ncblue"))
|
||||
.frame(width: 80, height: 80)
|
||||
Image(systemName: "square.text.square")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundStyle(Color.white)
|
||||
.padding(10)
|
||||
.background(Color("ncblue"))
|
||||
.frame(width: 80, height: 80)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 17))
|
||||
}
|
||||
Text(recipe.name)
|
||||
.font(.headline)
|
||||
.padding(.leading, 4)
|
||||
|
||||
Spacer()
|
||||
if let isDownloaded = isDownloaded {
|
||||
|
||||
@@ -66,23 +66,16 @@ struct RecipeDetailView: View {
|
||||
if(!recipeDetail.recipeIngredient.isEmpty) {
|
||||
RecipeIngredientSection(recipeDetail: recipeDetail)
|
||||
}
|
||||
if(!recipeDetail.tool.isEmpty) {
|
||||
RecipeListSection(title: "Tools", list: recipeDetail.tool)
|
||||
}
|
||||
if(!recipeDetail.recipeInstructions.isEmpty) {
|
||||
RecipeInstructionSection(recipeDetail: recipeDetail)
|
||||
}
|
||||
RecipeNutritionSection(recipeDetail: recipeDetail, presentNutritionPopover: $presentNutritionPopover)
|
||||
RecipeKeywordSection(recipeDetail: recipeDetail, presentKeywordPopover: $presentKeywordPopover)
|
||||
if(!recipeDetail.tool.isEmpty) {
|
||||
RecipeToolSection(recipeDetail: recipeDetail)
|
||||
}
|
||||
RecipeNutritionSection(recipeDetail: recipeDetail)
|
||||
RecipeKeywordSection(recipeDetail: recipeDetail)
|
||||
MoreInformationSection(recipeDetail: recipeDetail)
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
Text("Created: \(Date.convertISOStringToLocalString(isoDateString: recipeDetail.dateCreated) ?? "")")
|
||||
Text("Last modified: \(Date.convertISOStringToLocalString(isoDateString: recipeDetail.dateModified) ?? "")")
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundStyle(Color.secondary)
|
||||
.padding()
|
||||
}.padding(.horizontal, 5)
|
||||
|
||||
}
|
||||
@@ -151,7 +144,10 @@ fileprivate struct RecipeDurationSection: View {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 150), alignment: .leading)]) {
|
||||
if let prepTime = recipeDetail.prepTime, let time = DurationComponents.ptToText(prepTime) {
|
||||
VStack(alignment: .leading) {
|
||||
SecondaryLabel(text: LocalizedStringKey("Preparation"))
|
||||
HStack {
|
||||
SecondaryLabel(text: LocalizedStringKey("Preparation"))
|
||||
Spacer()
|
||||
}
|
||||
Text(time)
|
||||
.lineLimit(1)
|
||||
}.padding()
|
||||
@@ -159,7 +155,10 @@ fileprivate struct RecipeDurationSection: View {
|
||||
|
||||
if let cookTime = recipeDetail.cookTime, let time = DurationComponents.ptToText(cookTime) {
|
||||
VStack(alignment: .leading) {
|
||||
SecondaryLabel(text: LocalizedStringKey("Cooking"))
|
||||
HStack {
|
||||
SecondaryLabel(text: LocalizedStringKey("Cooking"))
|
||||
Spacer()
|
||||
}
|
||||
Text(time)
|
||||
.lineLimit(1)
|
||||
}.padding()
|
||||
@@ -167,7 +166,10 @@ fileprivate struct RecipeDurationSection: View {
|
||||
|
||||
if let totalTime = recipeDetail.totalTime, let time = DurationComponents.ptToText(totalTime) {
|
||||
VStack(alignment: .leading) {
|
||||
SecondaryLabel(text: LocalizedStringKey("Total time"))
|
||||
HStack {
|
||||
SecondaryLabel(text: LocalizedStringKey("Total time"))
|
||||
Spacer()
|
||||
}
|
||||
Text(time)
|
||||
.lineLimit(1)
|
||||
}.padding()
|
||||
@@ -180,76 +182,53 @@ fileprivate struct RecipeDurationSection: View {
|
||||
|
||||
fileprivate struct RecipeNutritionSection: View {
|
||||
@State var recipeDetail: RecipeDetail
|
||||
@Binding var presentNutritionPopover: Bool
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
presentNutritionPopover.toggle()
|
||||
} label: {
|
||||
HStack {
|
||||
SecondaryLabel(text: "Nutrition")
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundStyle(Color.secondary)
|
||||
.bold()
|
||||
Spacer()
|
||||
}.padding()
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.popover(isPresented: $presentNutritionPopover) {
|
||||
if let nutritionList = recipeDetail.getNutritionList() {
|
||||
ScrollView(showsIndicators: false) {
|
||||
if let servingSize = recipeDetail.nutrition["servingSize"] {
|
||||
RecipeListSection(title: "Nutrition (\(servingSize))", list: nutritionList)
|
||||
.presentationCompactAdaptation(.popover)
|
||||
HStack() {
|
||||
CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandNutritionSection) {
|
||||
Group {
|
||||
if let nutritionList = recipeDetail.getNutritionList() {
|
||||
RecipeListSection(list: nutritionList)
|
||||
} else {
|
||||
RecipeListSection(title: "Nutrition", list: nutritionList)
|
||||
.presentationCompactAdaptation(.popover)
|
||||
Text(LocalizedStringKey("No nutritional information."))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(LocalizedStringKey("No nutritional information."))
|
||||
.foregroundStyle(Color.secondary)
|
||||
.bold()
|
||||
.padding()
|
||||
.presentationCompactAdaptation(.popover)
|
||||
} title: {
|
||||
HStack {
|
||||
if let servingSize = recipeDetail.nutrition["servingSize"] {
|
||||
SecondaryLabel(text: "Nutrition (\(servingSize))")
|
||||
} else {
|
||||
SecondaryLabel(text: LocalizedStringKey("Nutrition"))
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fileprivate struct RecipeKeywordSection: View {
|
||||
@State var recipeDetail: RecipeDetail
|
||||
@Binding var presentKeywordPopover: Bool
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
presentKeywordPopover.toggle()
|
||||
} label: {
|
||||
HStack {
|
||||
SecondaryLabel(text: "Keywords")
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundStyle(Color.secondary)
|
||||
.bold()
|
||||
Spacer()
|
||||
}.padding()
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.popover(isPresented: $presentKeywordPopover) {
|
||||
if let keywords = getKeywords() {
|
||||
ScrollView(showsIndicators: false) {
|
||||
RecipeListSection(title: "Keywords", list: keywords)
|
||||
.presentationCompactAdaptation(.popover)
|
||||
CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandKeywordSection) {
|
||||
Group {
|
||||
if let keywords = getKeywords() {
|
||||
RecipeListSection(list: keywords)
|
||||
} else {
|
||||
Text(LocalizedStringKey("No keywords."))
|
||||
}
|
||||
} else {
|
||||
Text(LocalizedStringKey("No keywords."))
|
||||
.foregroundStyle(Color.secondary)
|
||||
.bold()
|
||||
.padding()
|
||||
.presentationCompactAdaptation(.popover)
|
||||
}
|
||||
|
||||
} title: {
|
||||
HStack {
|
||||
SecondaryLabel(text: LocalizedStringKey("Keywords"))
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
func getKeywords() -> [String]? {
|
||||
@@ -259,6 +238,35 @@ fileprivate struct RecipeKeywordSection: View {
|
||||
}
|
||||
|
||||
|
||||
fileprivate struct MoreInformationSection: View {
|
||||
let recipeDetail: RecipeDetail
|
||||
|
||||
var body: some View {
|
||||
CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandInfoSection) {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Created: \(Date.convertISOStringToLocalString(isoDateString: recipeDetail.dateCreated) ?? "")")
|
||||
Text("Last modified: \(Date.convertISOStringToLocalString(isoDateString: recipeDetail.dateModified) ?? "")")
|
||||
if recipeDetail.url != "", let url = URL(string: recipeDetail.url) {
|
||||
HStack() {
|
||||
Text("URL:")
|
||||
Link(destination: url) {
|
||||
Text(recipeDetail.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundStyle(Color.secondary)
|
||||
} title: {
|
||||
HStack {
|
||||
SecondaryLabel(text: "More information")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fileprivate struct RecipeIngredientSection: View {
|
||||
@State var recipeDetail: RecipeDetail
|
||||
@@ -283,6 +291,20 @@ fileprivate struct RecipeIngredientSection: View {
|
||||
}
|
||||
|
||||
|
||||
fileprivate struct RecipeToolSection: View {
|
||||
@State var recipeDetail: RecipeDetail
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
SecondaryLabel(text: "Tools")
|
||||
Spacer()
|
||||
}
|
||||
RecipeListSection(list: recipeDetail.tool)
|
||||
}.padding()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fileprivate struct IngredientListItem: View {
|
||||
@State var ingredient: String
|
||||
@@ -311,15 +333,10 @@ fileprivate struct IngredientListItem: View {
|
||||
|
||||
|
||||
fileprivate struct RecipeListSection: View {
|
||||
@State var title: LocalizedStringKey
|
||||
@State var list: [String]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
SecondaryLabel(text: title)
|
||||
Spacer()
|
||||
}
|
||||
ForEach(list, id: \.self) { item in
|
||||
HStack(alignment: .top) {
|
||||
Text("\u{2022}")
|
||||
@@ -328,12 +345,11 @@ fileprivate struct RecipeListSection: View {
|
||||
}
|
||||
.padding(4)
|
||||
}
|
||||
}.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fileprivate struct RecipeInstructionSection: View {
|
||||
@State var recipeDetail: RecipeDetail
|
||||
var body: some View {
|
||||
|
||||
@@ -32,6 +32,22 @@ struct SettingsView: View {
|
||||
Text("The selected cookbook will open on app launch by default.")
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle(isOn: $userSettings.expandNutritionSection) {
|
||||
Text("Expand nutrition section")
|
||||
}
|
||||
Toggle(isOn: $userSettings.expandKeywordSection) {
|
||||
Text("Expand keyword section")
|
||||
}
|
||||
Toggle(isOn: $userSettings.expandInfoSection) {
|
||||
Text("Expand information section")
|
||||
}
|
||||
} header: {
|
||||
Text("Recipes")
|
||||
} footer: {
|
||||
Text("Configure which sections in your recipes are expanded by default.")
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle(isOn: $userSettings.storeRecipes) {
|
||||
Text("Offline recipes")
|
||||
@@ -108,6 +124,12 @@ struct SettingsView: View {
|
||||
} message: {
|
||||
Text(alertType.getMessage())
|
||||
}
|
||||
.onDisappear {
|
||||
Task {
|
||||
userSettings.lastUpdate = .distantPast
|
||||
await viewModel.updateAllRecipeDetails()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user