diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj index a3722a3..0d85e80 100644 --- a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj +++ b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj @@ -46,7 +46,6 @@ A7FB0D7A2B25C66600A3469E /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7FB0D792B25C66600A3469E /* OnboardingView.swift */; }; A7FB0D7C2B25C68500A3469E /* TokenLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7FB0D7B2B25C68500A3469E /* TokenLoginView.swift */; }; A7FB0D7E2B25C6A200A3469E /* V2LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7FB0D7D2B25C6A200A3469E /* V2LoginView.swift */; }; - A95364672B7E89F1001018B0 /* ReorderableForEach.swift in Sources */ = {isa = PBXBuildFile; fileRef = A95364662B7E89F1001018B0 /* ReorderableForEach.swift */; }; A97506132B920D9F00E86029 /* RecipeDurationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97506122B920D9F00E86029 /* RecipeDurationSection.swift */; }; A97506152B920DF200E86029 /* RecipeGenericViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97506142B920DF200E86029 /* RecipeGenericViews.swift */; }; A97506192B920EC200E86029 /* RecipeIngredientSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97506182B920EC200E86029 /* RecipeIngredientSection.swift */; }; @@ -59,7 +58,7 @@ A977D0E22B60034E009783A9 /* GroceryListTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A977D0E12B60034E009783A9 /* GroceryListTabView.swift */; }; A97B4D322B80B3E900EC1A88 /* RecipeModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97B4D312B80B3E900EC1A88 /* RecipeModels.swift */; }; A97B4D352B80B82A00EC1A88 /* ShareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97B4D342B80B82A00EC1A88 /* ShareView.swift */; }; - A99DC7BC2B6411A7000118AA /* SimilaritySearchKit in Frameworks */ = {isa = PBXBuildFile; productRef = A99DC7BB2B6411A7000118AA /* SimilaritySearchKit */; }; + A9A43AE12B963150003D95CA /* SwipeActions in Frameworks */ = {isa = PBXBuildFile; productRef = A9A43AE02B963150003D95CA /* SwipeActions */; }; A9BBB38C2B8D3B0C002DA7FF /* ParallaxHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BBB38B2B8D3B0C002DA7FF /* ParallaxHeaderView.swift */; }; A9BBB38E2B8E44B3002DA7FF /* BottomClipper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BBB38D2B8E44B3002DA7FF /* BottomClipper.swift */; }; A9BBB3902B91BE31002DA7FF /* ObservableRecipeDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BBB38F2B91BE31002DA7FF /* ObservableRecipeDetail.swift */; }; @@ -129,7 +128,6 @@ A7FB0D792B25C66600A3469E /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; A7FB0D7B2B25C68500A3469E /* TokenLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenLoginView.swift; sourceTree = ""; }; A7FB0D7D2B25C6A200A3469E /* V2LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = V2LoginView.swift; sourceTree = ""; }; - A95364662B7E89F1001018B0 /* ReorderableForEach.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableForEach.swift; sourceTree = ""; }; A97506122B920D9F00E86029 /* RecipeDurationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeDurationSection.swift; sourceTree = ""; }; A97506142B920DF200E86029 /* RecipeGenericViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeGenericViews.swift; sourceTree = ""; }; A97506182B920EC200E86029 /* RecipeIngredientSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeIngredientSection.swift; sourceTree = ""; }; @@ -157,7 +155,7 @@ buildActionMask = 2147483647; files = ( A74D33BE2AF82AAE00D06555 /* SwiftSoup in Frameworks */, - A99DC7BC2B6411A7000118AA /* SimilaritySearchKit in Frameworks */, + A9A43AE12B963150003D95CA /* SwipeActions in Frameworks */, A9CA6CF62B4C63F200F78AB5 /* TPPDF in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -409,7 +407,6 @@ children = ( A9BBB38B2B8D3B0C002DA7FF /* ParallaxHeaderView.swift */, A7CD3FD12B2C546A00D764AD /* CollapsibleView.swift */, - A95364662B7E89F1001018B0 /* ReorderableForEach.swift */, A9BBB38D2B8E44B3002DA7FF /* BottomClipper.swift */, ); path = ReusableViews; @@ -450,7 +447,7 @@ packageProductDependencies = ( A74D33BD2AF82AAE00D06555 /* SwiftSoup */, A9CA6CF52B4C63F200F78AB5 /* TPPDF */, - A99DC7BB2B6411A7000118AA /* SimilaritySearchKit */, + A9A43AE02B963150003D95CA /* SwipeActions */, ); productName = "Nextcloud Cookbook iOS Client"; productReference = A701717E2AA8E71900064C43 /* Nextcloud Cookbook iOS Client.app */; @@ -530,7 +527,7 @@ packageReferences = ( A74D33BC2AF82AAE00D06555 /* XCRemoteSwiftPackageReference "SwiftSoup" */, A9CA6CF42B4C63F200F78AB5 /* XCRemoteSwiftPackageReference "TPPDF" */, - A99DC7BA2B6411A7000118AA /* XCRemoteSwiftPackageReference "similarity-search-kit" */, + A9A43ADF2B963150003D95CA /* XCRemoteSwiftPackageReference "SwipeActions" */, ); productRefGroup = A701717F2AA8E71900064C43 /* Products */; projectDirPath = ""; @@ -594,7 +591,6 @@ A70171C42AB4A31200064C43 /* DataStore.swift in Sources */, A7FB0D7E2B25C6A200A3469E /* V2LoginView.swift in Sources */, A975061D2B920FCC00E86029 /* RecipeInstructionSection.swift in Sources */, - A95364672B7E89F1001018B0 /* ReorderableForEach.swift in Sources */, A787B0782B2B1E6400C2DF1B /* DateExtension.swift in Sources */, A79AA8E92B062DD1007D25F2 /* CookbookApiV1.swift in Sources */, A7CD3FD22B2C546A00D764AD /* CollapsibleView.swift in Sources */, @@ -1009,12 +1005,12 @@ minimumVersion = 2.6.1; }; }; - A99DC7BA2B6411A7000118AA /* XCRemoteSwiftPackageReference "similarity-search-kit" */ = { + A9A43ADF2B963150003D95CA /* XCRemoteSwiftPackageReference "SwipeActions" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/ZachNagengast/similarity-search-kit"; + repositoryURL = "https://github.com/aheze/SwipeActions"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.0.13; + minimumVersion = 1.1.0; }; }; A9CA6CF42B4C63F200F78AB5 /* XCRemoteSwiftPackageReference "TPPDF" */ = { @@ -1033,10 +1029,10 @@ package = A74D33BC2AF82AAE00D06555 /* XCRemoteSwiftPackageReference "SwiftSoup" */; productName = SwiftSoup; }; - A99DC7BB2B6411A7000118AA /* SimilaritySearchKit */ = { + A9A43AE02B963150003D95CA /* SwipeActions */ = { isa = XCSwiftPackageProductDependency; - package = A99DC7BA2B6411A7000118AA /* XCRemoteSwiftPackageReference "similarity-search-kit" */; - productName = SimilaritySearchKit; + package = A9A43ADF2B963150003D95CA /* XCRemoteSwiftPackageReference "SwipeActions" */; + productName = SwipeActions; }; A9CA6CF52B4C63F200F78AB5 /* TPPDF */ = { isa = XCSwiftPackageProductDependency; diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7f6ada7..98ab635 100644 --- a/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,14 +1,5 @@ { "pins" : [ - { - "identity" : "similarity-search-kit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ZachNagengast/similarity-search-kit", - "state" : { - "revision" : "ddc8e458d0e826b2fe5dbce6f6eac96a8935e8eb", - "version" : "0.0.13" - } - }, { "identity" : "swiftsoup", "kind" : "remoteSourceControl", @@ -18,6 +9,15 @@ "version" : "2.6.1" } }, + { + "identity" : "swipeactions", + "kind" : "remoteSourceControl", + "location" : "https://github.com/aheze/SwipeActions", + "state" : { + "revision" : "41e6f6dce02d8cfa164f8c5461a41340850ca3ab", + "version" : "1.1.0" + } + }, { "identity" : "tppdf", "kind" : "remoteSourceControl", diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate b/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate index 01af19e..d9cc0c8 100644 Binary files a/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate and b/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/WorkspaceSettings.xcsettings b/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..bbfef02 --- /dev/null +++ b/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/WorkspaceSettings.xcsettings @@ -0,0 +1,14 @@ + + + + + BuildLocationStyle + UseAppPreferences + CustomBuildLocationType + RelativeToDerivedData + DerivedDataLocationStyle + Default + ShowSharedSchemesAutomaticallyEnabled + + + diff --git a/Nextcloud Cookbook iOS Client/Data/DataStore.swift b/Nextcloud Cookbook iOS Client/Data/DataStore.swift index 26472cb..4ef9bd0 100644 --- a/Nextcloud Cookbook iOS Client/Data/DataStore.swift +++ b/Nextcloud Cookbook iOS Client/Data/DataStore.swift @@ -89,26 +89,5 @@ class DataStore { } } -// SimilarityIndex loading and saving -import SimilaritySearchKit -extension DataStore { - func loadIndex() async -> [IndexItem]? { - do { - let indexItems = try await SimilarityIndex().loadIndex(fromDirectory: Self.fileURL(appending: "similarity_index")) - return indexItems - } catch { - print("Unable to load SimilarityIndex") - return nil - } - } - - func saveIndex(_ index: SimilarityIndex) { - do { - try index.saveIndex(toDirectory: Self.fileURL(appending: "similarity_index")) - } catch { - print("Unable to save SimilarityIndex") - } - } -} diff --git a/Nextcloud Cookbook iOS Client/Data/ObservableRecipeDetail.swift b/Nextcloud Cookbook iOS Client/Data/ObservableRecipeDetail.swift index 64bae72..18cc166 100644 --- a/Nextcloud Cookbook iOS Client/Data/ObservableRecipeDetail.swift +++ b/Nextcloud Cookbook iOS Client/Data/ObservableRecipeDetail.swift @@ -20,9 +20,9 @@ class ObservableRecipeDetail: ObservableObject { @Published var url: String @Published var recipeYield: Int @Published var recipeCategory: String - @Published var tool: [ReorderableItem] - @Published var recipeIngredient: [ReorderableItem] - @Published var recipeInstructions: [ReorderableItem] + @Published var tool: [String] + @Published var recipeIngredient: [String] + @Published var recipeInstructions: [String] @Published var nutrition: [String:String] init() { @@ -55,9 +55,9 @@ class ObservableRecipeDetail: ObservableObject { url = recipeDetail.url recipeYield = recipeDetail.recipeYield recipeCategory = recipeDetail.recipeCategory - tool = ReorderableItem.list(items: recipeDetail.tool) - recipeIngredient = ReorderableItem.list(items: recipeDetail.recipeIngredient) - recipeInstructions = ReorderableItem.list(items: recipeDetail.recipeInstructions) + tool = recipeDetail.tool + recipeIngredient = recipeDetail.recipeIngredient + recipeInstructions = recipeDetail.recipeInstructions nutrition = recipeDetail.nutrition } @@ -76,9 +76,9 @@ class ObservableRecipeDetail: ObservableObject { url: self.url, recipeYield: self.recipeYield, recipeCategory: self.recipeCategory, - tool: ReorderableItem.items(self.tool), - recipeIngredient: ReorderableItem.items(self.recipeIngredient), - recipeInstructions: ReorderableItem.items(self.recipeInstructions), + tool: self.tool, + recipeIngredient: self.recipeIngredient, + recipeInstructions: self.recipeInstructions, nutrition: self.nutrition ) } diff --git a/Nextcloud Cookbook iOS Client/Extensions/ColorExtension.swift b/Nextcloud Cookbook iOS Client/Extensions/ColorExtension.swift index 7f55d8d..66136e6 100644 --- a/Nextcloud Cookbook iOS Client/Extensions/ColorExtension.swift +++ b/Nextcloud Cookbook iOS Client/Extensions/ColorExtension.swift @@ -12,6 +12,9 @@ extension Color { public static var nextcloudBlue: Color { return Color("ncblue") } + public static var nextcloudDarkBlue: Color { + return Color("ncdarkblue") + } public static var backgroundHighlight: Color { return Color("backgroundHighlight") } diff --git a/Nextcloud Cookbook iOS Client/Localizable.xcstrings b/Nextcloud Cookbook iOS Client/Localizable.xcstrings index b7e583f..dbcc04b 100644 --- a/Nextcloud Cookbook iOS Client/Localizable.xcstrings +++ b/Nextcloud Cookbook iOS Client/Localizable.xcstrings @@ -230,6 +230,7 @@ }, "%lld." : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -407,6 +408,7 @@ } }, "Add" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -427,6 +429,9 @@ } } } + }, + "Add cooking steps for fellow chefs to follow." : { + }, "Add groceries to this list by either using the button next to an ingredient list in a recipe, or by swiping right on individual ingredients of a recipe." : { "localizations" : { @@ -634,6 +639,7 @@ } }, "Category: %@" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -816,6 +822,7 @@ } }, "Cooking duration:" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -1056,9 +1063,6 @@ } } } - }, - "Disable deletion" : { - }, "Done" : { "localizations" : { @@ -1180,9 +1184,6 @@ } } } - }, - "Enable deletion" : { - }, "Error" : { "localizations" : { @@ -1525,6 +1526,7 @@ } }, "Import recipe from a website" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -1755,6 +1757,9 @@ } } } + }, + "List your tools here. 🍴" : { + }, "Log out" : { "localizations" : { @@ -1982,6 +1987,7 @@ } }, "New recipe" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -2359,6 +2365,7 @@ } }, "Preparation duration:" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -2564,9 +2571,6 @@ } } } - }, - "Select Item" : { - }, "Select Keywords" : { @@ -2600,6 +2604,7 @@ }, "Servings:" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -2733,6 +2738,9 @@ }, "Sodium content" : { "comment" : "Sodium content" + }, + "Start by adding your first ingredient! 🥬" : { + }, "Store recipe images locally" : { "localizations" : { @@ -3026,6 +3034,7 @@ } }, "Title" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -3071,11 +3080,9 @@ } } } - }, - "Total" : { - }, "Total duration:" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -3236,6 +3243,7 @@ "comment" : "Unsaturated fat content" }, "Upload" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { diff --git a/Nextcloud Cookbook iOS Client/Util/DurationComponents.swift b/Nextcloud Cookbook iOS Client/Util/DurationComponents.swift index 3eb2fc5..b4416ba 100644 --- a/Nextcloud Cookbook iOS Client/Util/DurationComponents.swift +++ b/Nextcloud Cookbook iOS Client/Util/DurationComponents.swift @@ -137,4 +137,10 @@ class DurationComponents: ObservableObject { } } + static func + (lhs: DurationComponents, rhs: DurationComponents) -> DurationComponents { + let totalSeconds = lhs.toSeconds() + rhs.toSeconds() + let result = DurationComponents() + result.fromSeconds(Int(totalSeconds)) + return result + } } diff --git a/Nextcloud Cookbook iOS Client/Views/MainView.swift b/Nextcloud Cookbook iOS Client/Views/MainView.swift index 2c35775..81ad244 100644 --- a/Nextcloud Cookbook iOS Client/Views/MainView.swift +++ b/Nextcloud Cookbook iOS Client/Views/MainView.swift @@ -6,7 +6,6 @@ // import SwiftUI -import SimilaritySearchKit struct MainView: View { @StateObject var appState = AppState() diff --git a/Nextcloud Cookbook iOS Client/Views/RecipeEditing/CategoryPickerViewOld.swift b/Nextcloud Cookbook iOS Client/Views/RecipeEditing/CategoryPickerViewOld.swift index 27bc070..2e2253d 100644 --- a/Nextcloud Cookbook iOS Client/Views/RecipeEditing/CategoryPickerViewOld.swift +++ b/Nextcloud Cookbook iOS Client/Views/RecipeEditing/CategoryPickerViewOld.swift @@ -9,7 +9,7 @@ import Foundation import SwiftUI - +/* struct CategoryPickerViewOld: View { @State var title: String @State var searchSuggestions: [String] @@ -61,3 +61,4 @@ struct CategoryPickerViewOld: View { } } +*/ diff --git a/Nextcloud Cookbook iOS Client/Views/RecipeEditing/RecipeEditView.swift b/Nextcloud Cookbook iOS Client/Views/RecipeEditing/RecipeEditView.swift index 8698e76..92a090a 100644 --- a/Nextcloud Cookbook iOS Client/Views/RecipeEditing/RecipeEditView.swift +++ b/Nextcloud Cookbook iOS Client/Views/RecipeEditing/RecipeEditView.swift @@ -10,7 +10,7 @@ import SwiftUI import PhotosUI - +/* struct RecipeEditView: View { @ObservedObject var viewModel: RecipeEditViewModel @Binding var isPresented: Bool @@ -279,4 +279,4 @@ fileprivate struct DurationPicker: View { - +*/ diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeView.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeView.swift index 532b550..6fd44e4 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeView.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeView.swift @@ -34,6 +34,16 @@ struct RecipeView: View { .scaledToFill() .frame(maxHeight: imageHeight + 200) .clipped() + } else { + Rectangle() + .frame(height: 400) + .foregroundStyle( + LinearGradient( + gradient: Gradient(colors: [.white, .nextcloudBlue]), + startPoint: .top, + endPoint: .bottom + ) + ) } } @@ -41,6 +51,11 @@ struct RecipeView: View { if viewModel.editMode { RecipeImportSection(viewModel: viewModel, importRecipe: importRecipe) } + + if viewModel.editMode { + RecipeMetadataSection(viewModel: viewModel) + } + HStack { EditableText(text: $viewModel.observableRecipeDetail.name, editMode: $viewModel.editMode, titleKey: "Recipe Name") .font(.title) @@ -66,10 +81,6 @@ struct RecipeView: View { Divider() - if viewModel.editMode { - RecipeMetadataSection(viewModel: viewModel) - } - LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) { if(!viewModel.observableRecipeDetail.recipeIngredient.isEmpty || viewModel.editMode) { RecipeIngredientSection(viewModel: viewModel) @@ -83,13 +94,12 @@ struct RecipeView: View { RecipeNutritionSection(viewModel: viewModel) } - Divider() - - LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) { - if !viewModel.editMode { + if !viewModel.editMode { + Divider() + LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) { RecipeKeywordSection(viewModel: viewModel) + MoreInformationSection(viewModel: viewModel) } - MoreInformationSection(viewModel: viewModel) } } .padding(.horizontal, 5) @@ -99,6 +109,7 @@ struct RecipeView: View { .coordinateSpace(name: CoordinateSpaces.scrollView) .ignoresSafeArea(.container, edges: .top) .navigationBarTitleDisplayMode(.inline) + //.toolbarTitleDisplayMode(.inline) .navigationTitle(viewModel.showTitle ? viewModel.recipe.name : "") .toolbar { if viewModel.editMode { @@ -112,7 +123,6 @@ struct RecipeView: View { // Upload Button ToolbarItem(placement: .topBarTrailing) { Button { - // TODO: POST edited recipe Task { if viewModel.newRecipe { if let res = await uploadNewRecipe() { @@ -191,6 +201,7 @@ struct RecipeView: View { } label: { Image(systemName: "ellipsis.circle") } + } } } @@ -199,6 +210,36 @@ struct RecipeView: View { recipeImage: viewModel.recipeImage, presentShareSheet: $viewModel.presentShareSheet) } + .sheet(isPresented: $viewModel.presentInstructionEditView) { + EditableListView( + isPresented: $viewModel.presentInstructionEditView, + items: $viewModel.observableRecipeDetail.recipeInstructions, + title: "Instructions", + emptyListText: "Add cooking steps for fellow chefs to follow.", + titleKey: "Instruction", + lineLimit: 0...10, + axis: .vertical) + } + .sheet(isPresented: $viewModel.presentIngredientEditView) { + EditableListView( + isPresented: $viewModel.presentIngredientEditView, + items: $viewModel.observableRecipeDetail.recipeIngredient, + title: "Ingredients", + emptyListText: "Start by adding your first ingredient! 🥬", + titleKey: "Ingredient", + lineLimit: 0...1, + axis: .horizontal) + } + .sheet(isPresented: $viewModel.presentToolEditView) { + EditableListView( + isPresented: $viewModel.presentToolEditView, + items: $viewModel.observableRecipeDetail.tool, + title: "Tools", + emptyListText: "List your tools here. 🍴", + titleKey: "Tool", + lineLimit: 0...1, + axis: .horizontal) + } .task { // Load recipe detail @@ -282,10 +323,15 @@ struct RecipeView: View { @Published var recipeDetail: RecipeDetail = RecipeDetail.error @Published var recipeImage: UIImage? = nil @Published var editMode: Bool = false - @Published var presentShareSheet: Bool = false @Published var showTitle: Bool = false @Published var isDownloaded: Bool? = nil @Published var importUrl: String = "" + + @Published var presentShareSheet: Bool = false + @Published var presentInstructionEditView: Bool = false + @Published var presentIngredientEditView: Bool = false + @Published var presentToolEditView: Bool = false + var recipe: Recipe var sharedURL: URL? = nil var newRecipe: Bool = false @@ -408,26 +454,28 @@ fileprivate struct RecipeImportSection: View { .font(.caption) .foregroundStyle(.secondary) - HStack { + TextField(LocalizedStringKey("URL (e.g. example.com/recipe)"), text: $viewModel.importUrl) .textFieldStyle(.roundedBorder) - Button { - Task { - if let res = await importRecipe(viewModel.importUrl) { - viewModel.alertType = RecipeAlert.CUSTOM( - title: res.localizedTitle, - description: res.localizedDescription - ) - viewModel.alertAction = { } - viewModel.presentAlert = true - } + .padding(.top, 5) + Button { + Task { + if let res = await importRecipe(viewModel.importUrl) { + viewModel.alertType = RecipeAlert.CUSTOM( + title: res.localizedTitle, + description: res.localizedDescription + ) + viewModel.alertAction = { } + viewModel.presentAlert = true } - } label: { - Text(LocalizedStringKey("Import")) } - }.padding(.top, 5) + } label: { + Text(LocalizedStringKey("Import")) + } + .buttonStyle(.bordered) } .padding() - .background(Rectangle().foregroundStyle(Color.white.opacity(0.1))) + .background(RoundedRectangle(cornerRadius: 20).foregroundStyle(Color.white.opacity(0.1))) + .padding() } } diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeDurationSection.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeDurationSection.swift index 48172d8..08c8c69 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeDurationSection.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeDurationSection.swift @@ -15,40 +15,28 @@ struct RecipeDurationSection: View { @State var presentPopover: Bool = false var body: some View { - if !viewModel.editMode { + VStack(alignment: .leading) { LazyVGrid(columns: [GridItem(.adaptive(minimum: 200, maximum: .infinity), alignment: .leading)]) { DurationView(time: viewModel.observableRecipeDetail.prepTime, title: LocalizedStringKey("Preparation")) DurationView(time: viewModel.observableRecipeDetail.cookTime, title: LocalizedStringKey("Cooking")) DurationView(time: viewModel.observableRecipeDetail.totalTime, title: LocalizedStringKey("Total time")) } - } else { - LazyVGrid(columns: [GridItem(.adaptive(minimum: 200, maximum: .infinity), alignment: .leading)]) { - Button { - presentPopover.toggle() - } label: { - DurationView(time: viewModel.observableRecipeDetail.prepTime, title: LocalizedStringKey("Preparation")) - } - Button { - presentPopover.toggle() - } label: { - DurationView(time: viewModel.observableRecipeDetail.cookTime, title: LocalizedStringKey("Cooking")) - } - Button { - presentPopover.toggle() - } label: { - DurationView(time: viewModel.observableRecipeDetail.totalTime, title: LocalizedStringKey("Total time")) - } - } - .popover(isPresented: $presentPopover) { - EditableDurationView( - prepTime: viewModel.observableRecipeDetail.prepTime, - cookTime: viewModel.observableRecipeDetail.cookTime, - totalTime: viewModel.observableRecipeDetail.totalTime - ) + Button { + presentPopover.toggle() + } label: { + Text("Edit") } + .buttonStyle(.borderedProminent) + .padding(.top, 5) + } + .padding() + .popover(isPresented: $presentPopover) { + EditableDurationView( + prepTime: viewModel.observableRecipeDetail.prepTime, + cookTime: viewModel.observableRecipeDetail.cookTime, + totalTime: viewModel.observableRecipeDetail.totalTime + ) } - - } } @@ -64,36 +52,64 @@ fileprivate struct DurationView: View { } HStack { Image(systemName: "clock") + .bold() .foregroundStyle(.secondary) Text(time.displayString) .lineLimit(1) } } - .padding() } } fileprivate struct EditableDurationView: View { + @Environment(\.presentationMode) var presentationMode @ObservedObject var prepTime: DurationComponents @ObservedObject var cookTime: DurationComponents @ObservedObject var totalTime: DurationComponents var body: some View { ScrollView { - VStack(alignment: .leading) { + VStack(alignment: .center) { HStack { SecondaryLabel(text: "Preparation") Spacer() + Button("Done") { + presentationMode.wrappedValue.dismiss() + } } TimePickerView(selectedHour: $prepTime.hourComponent, selectedMinute: $prepTime.minuteComponent) - SecondaryLabel(text: "Cooking") + + HStack { + SecondaryLabel(text: "Cooking") + Spacer() + } TimePickerView(selectedHour: $cookTime.hourComponent, selectedMinute: $cookTime.minuteComponent) - SecondaryLabel(text: "Total") + + HStack { + SecondaryLabel(text: "Total time") + Spacer() + } TimePickerView(selectedHour: $totalTime.hourComponent, selectedMinute: $totalTime.minuteComponent) } .padding() + .onChange(of: prepTime.hourComponent) { _ in updateTotalTime() } + .onChange(of: prepTime.minuteComponent) { _ in updateTotalTime() } + .onChange(of: cookTime.hourComponent) { _ in updateTotalTime() } + .onChange(of: cookTime.minuteComponent) { _ in updateTotalTime() } } } + + private func updateTotalTime() { + var hourComponent = prepTime.hourComponent + cookTime.hourComponent + var minuteComponent = prepTime.minuteComponent + cookTime.minuteComponent + // Handle potential overflow from minutes to hours + if minuteComponent >= 60 { + hourComponent += minuteComponent / 60 + minuteComponent %= 60 + } + totalTime.hourComponent = hourComponent + totalTime.minuteComponent = minuteComponent + } } diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeGenericViews.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeGenericViews.swift index e19793c..b556489 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeGenericViews.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeGenericViews.swift @@ -59,27 +59,93 @@ struct EditableText: View { } -struct EditableStringList: View { - @Binding var items: [ReorderableItem] - @Binding var editMode: Bool +struct EditableListView: View { + @Binding var isPresented: Bool + @Binding var items: [String] + @State var title: LocalizedStringKey + @State var emptyListText: LocalizedStringKey @State var titleKey: LocalizedStringKey = "" @State var lineLimit: ClosedRange = 0...50 @State var axis: Axis = .vertical - var content: () -> Content - var body: some View { - if editMode { - VStack { - ReorderableForEach(items: $items, defaultItem: ReorderableItem(item: "")) { ix, item in - TextField("", text: $items[ix].item, axis: axis) - .textFieldStyle(.roundedBorder) - .lineLimit(lineLimit) + NavigationView { + ZStack { + List { + if items.isEmpty { + Text(emptyListText) + } + + ForEach(items.indices, id: \.self) { ix in + TextField(titleKey, text: $items[ix], axis: axis) + .lineLimit(lineLimit) + } + .onDelete(perform: deleteItem) + .onMove(perform: moveItem) + } + VStack { + Spacer() + + Button { + addItem() + } label: { + Image(systemName: "plus") + .foregroundStyle(.white) + .bold() + .padding() + .background(Circle().fill(Color.nextcloudBlue)) + } + .padding() } } - .transition(.slide) - } else { - content() + .navigationBarTitle(title, displayMode: .inline) + .navigationBarItems( + trailing: Button(action: { isPresented = false }) { + Text("Done") + } + ) + .environment(\.editMode, .constant(.active)) // Bind edit mode to your state variable + } + } + + private func addItem() { + withAnimation { + items.append("") + } + } + + private func deleteItem(at offsets: IndexSet) { + withAnimation { + items.remove(atOffsets: offsets) + } + } + + private func moveItem(from source: IndexSet, to destination: Int) { + withAnimation { + items.move(fromOffsets: source, toOffset: destination) } } } + + + +// MARK: - Previews + +struct EditableListView_Previews: PreviewProvider { + // Sample keywords for preview + @State static var sampleList: [String] = [ + /*"3 Eggs", + "1 kg Potatos", + "3 g Sugar", + "1 ml Milk", + "Salt, Pepper"*/ + ] + + static var previews: some View { + Color.white + .sheet(isPresented: .constant(true), content: { + EditableListView(isPresented: .constant(true), items: $sampleList, title: "Ingredient", emptyListText: "Add cooking steps for fellow chefs to follow.") + }) + + } +} diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeIngredientSection.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeIngredientSection.swift index a68f3d9..81aa350 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeIngredientSection.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeIngredientSection.swift @@ -31,7 +31,7 @@ struct RecipeIngredientSection: View { groceryList.deleteGroceryRecipe(viewModel.observableRecipeDetail.id) } else { groceryList.addItems( - ReorderableItem.items(viewModel.observableRecipeDetail.recipeIngredient), + viewModel.observableRecipeDetail.recipeIngredient, toRecipe: viewModel.observableRecipeDetail.id, recipeName: viewModel.observableRecipeDetail.name ) @@ -46,17 +46,23 @@ struct RecipeIngredientSection: View { } } - EditableStringList(items: $viewModel.observableRecipeDetail.recipeIngredient, editMode: $viewModel.editMode, titleKey: "Ingredient", lineLimit: 0...1, axis: .horizontal) { - ForEach(0.. + @Binding var ingredient: String @State var recipeId: String let addToGroceryListAction: () -> Void @State var isSelected: Bool = false @@ -78,7 +84,7 @@ fileprivate struct IngredientListItem: View { var body: some View { HStack(alignment: .top) { - if groceryList.containsItem(at: recipeId, item: ingredient.item) { + if groceryList.containsItem(at: recipeId, item: ingredient) { if #available(iOS 17.0, *) { Image(systemName: "storefront") .foregroundStyle(Color.green) @@ -93,7 +99,7 @@ fileprivate struct IngredientListItem: View { Image(systemName: "circle") } - Text("\(ingredient.item)") + Text("\(ingredient)") .multilineTextAlignment(.leading) .lineLimit(5) Spacer() @@ -119,15 +125,13 @@ fileprivate struct IngredientListItem: View { .onEnded { gesture in withAnimation { if dragOffset > maxDragDistance * 0.3 { // Swipe threshold - if groceryList.containsItem(at: recipeId, item: ingredient.item) { - groceryList.deleteItem(ingredient.item, fromRecipe: recipeId) + if groceryList.containsItem(at: recipeId, item: ingredient) { + groceryList.deleteItem(ingredient, fromRecipe: recipeId) } else { addToGroceryListAction() } - } // Animate back to original position - self.dragOffset = 0 self.animationStartOffset = 0 } diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeInstructionSection.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeInstructionSection.swift index 72ba0fa..4409f4b 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeInstructionSection.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeInstructionSection.swift @@ -19,19 +19,27 @@ struct RecipeInstructionSection: View { SecondaryLabel(text: LocalizedStringKey("Instructions")) Spacer() } - EditableStringList(items: $viewModel.observableRecipeDetail.recipeInstructions, editMode: $viewModel.editMode, titleKey: "Instruction", lineLimit: 0...15, axis: .vertical) { - ForEach(0.. + @Binding var instruction: String @State var index: Int @State var isSelected: Bool = false @@ -39,7 +47,7 @@ fileprivate struct RecipeInstructionListItem: View { HStack(alignment: .top) { Text("\(index)") .monospaced() - Text(instruction.item) + Text(instruction) }.padding(4) .foregroundStyle(isSelected ? Color.secondary : Color.primary) .onTapGesture { diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeMetadataSection.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeMetadataSection.swift index a2bad4e..0130f34 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeMetadataSection.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeMetadataSection.swift @@ -19,7 +19,6 @@ struct RecipeMetadataSection: View { appState.categories.map({ category in category.name }) } - @State var presentKeywordSheet: Bool = false @State var presentServingsPopover: Bool = false @State var presentCategoryPopover: Bool = false @@ -27,7 +26,6 @@ struct RecipeMetadataSection: View { var body: some View { VStack(alignment: .leading) { // Category - //CategoryPickerView(items: $categories, input: $viewModel.observableRecipeDetail.recipeCategory, titleKey: "Category") SecondaryLabel(text: "Category") HStack { TextField("Category", text: $viewModel.observableRecipeDetail.recipeCategory) @@ -42,6 +40,7 @@ struct RecipeMetadataSection: View { } .pickerStyle(.menu) } + .padding(.bottom) // Keywords SecondaryLabel(text: "Keywords") @@ -51,6 +50,8 @@ struct RecipeMetadataSection: View { HStack { ForEach(viewModel.observableRecipeDetail.keywords, id: \.self) { keyword in Text(keyword) + .padding(5) + .background(RoundedRectangle(cornerRadius: 20).foregroundStyle(Color.primary.opacity(0.1))) } } } @@ -61,6 +62,7 @@ struct RecipeMetadataSection: View { Text("Select Keywords") Image(systemName: "chevron.right") } + .padding(.bottom) // Servings / Yield VStack(alignment: .leading) { @@ -77,7 +79,8 @@ struct RecipeMetadataSection: View { } } .padding() - .background(Rectangle().foregroundStyle(Color.white.opacity(0.1))) + .background(RoundedRectangle(cornerRadius: 20).foregroundStyle(Color.white.opacity(0.1))) + .padding() .sheet(isPresented: $presentKeywordSheet) { KeywordPickerView(title: "Keywords", searchSuggestions: appState.allKeywords, selection: $viewModel.observableRecipeDetail.keywords) } @@ -104,47 +107,6 @@ fileprivate struct PickerPopoverView: View { - @Binding var items: [ReorderableItem] - var defaultItem: ReorderableItem - var content: (Int, Item) -> Content - - @State var draggedItemId: UUID? = nil - @State var allowDeletion: Bool = false - - var body: some View { - VStack { - ForEach(Array(zip(items.indices, items)), id: \.1.id) { ix, item in - HStack { - if allowDeletion { - Button { - items.remove(at: ix) - } label: { - Image(systemName: "minus.circle.fill") - .foregroundColor(.red) - .padding(5) - .bold() - }.buttonStyle(.plain) - } - HStack { - content(ix, item.item) - Image(systemName: "line.3.horizontal") - .padding(5) - } - .padding(5) - .background( - RoundedRectangle(cornerRadius: 10) - .foregroundStyle(.background) - .ignoresSafeArea() - ) - } - .onDrag { - self.draggedItemId = item.id - return NSItemProvider(item: nil, typeIdentifier: item.id.uuidString) - } preview: { - EmptyView() - } - .onDrop(of: [.plainText], delegate: DropViewDelegate(targetId: item.id, sourceId: $draggedItemId, items: $items)) - } - HStack { - Button { - allowDeletion.toggle() - } label: { - Text(allowDeletion ? "Disable deletion" : "Enable deletion") - .bold() - } - .tint(Color.red) - Spacer() - Button { - items.append(defaultItem) - } label: { - Image(systemName: "plus") - .bold() - .padding(.vertical, 2) - .padding(.horizontal) - } - .buttonStyle(.borderedProminent) - }.padding(.top, 3) - }.animation(.default, value: allowDeletion) - } -} - - -struct ReorderableItem: Identifiable { - let id = UUID() - var item: Item - - static func list(items: [Item]) -> [ReorderableItem] { - items.map({ item in ReorderableItem(item: item) }) - } - - static func items(_ reorderableItems: [ReorderableItem]) -> [Item] { - reorderableItems.map { $0.item } - } -} - - -struct DropViewDelegate: DropDelegate { - let targetId: UUID - @Binding var sourceId : UUID? - @Binding var items: [ReorderableItem] - - func performDrop(info: DropInfo) -> Bool { - return true - } - - func dropEntered(info: DropInfo) { - guard let sourceId = self.sourceId else { - return - } - - if sourceId != targetId { - guard let sourceIndex = items.firstIndex(where: { $0.id == sourceId }), - let targetIndex = items.firstIndex(where: { $0.id == targetId }) - else { return } - withAnimation(.default) { - self.items.move(fromOffsets: IndexSet(integer: sourceIndex), toOffset: targetIndex > sourceIndex ? targetIndex + 1 : targetIndex) - } - } - } -} diff --git a/Nextcloud Cookbook iOS Client/Views/Tabs/RecipeTabView.swift b/Nextcloud Cookbook iOS Client/Views/Tabs/RecipeTabView.swift index 2784ced..3fc5837 100644 --- a/Nextcloud Cookbook iOS Client/Views/Tabs/RecipeTabView.swift +++ b/Nextcloud Cookbook iOS Client/Views/Tabs/RecipeTabView.swift @@ -65,17 +65,6 @@ struct RecipeTabView: View { } } .tint(.nextcloudBlue) - - /*.sheet(isPresented: $viewModel.presentEditView) { - RecipeEditView( - viewModel: - RecipeEditViewModel( - mainViewModel: mainViewModel, - uploadNew: true - ), - isPresented: $viewModel.presentEditView - ) - }*/ .task { viewModel.serverConnection = await mainViewModel.checkServerConnection() } diff --git a/Nextcloud Cookbook iOS Client/Views/Tabs/SearchTabView.swift b/Nextcloud Cookbook iOS Client/Views/Tabs/SearchTabView.swift index 0da525b..7a4cfae 100644 --- a/Nextcloud Cookbook iOS Client/Views/Tabs/SearchTabView.swift +++ b/Nextcloud Cookbook iOS Client/Views/Tabs/SearchTabView.swift @@ -7,7 +7,6 @@ import Foundation import SwiftUI -import SimilaritySearchKit struct SearchTabView: View { @EnvironmentObject var viewModel: SearchTabView.ViewModel @@ -56,8 +55,7 @@ struct SearchTabView: View { @Published var searchText: String = "" @Published var searchMode: SearchMode = .name - var similarityIndex: SimilarityIndex? = nil - var similaritySearchResults: [SearchResult] = [] + enum SearchMode: String, CaseIterable { case name = "Name & Keywords", ingredient = "Ingredients"