Recipe edit UI polish

This commit is contained in:
VincentMeilinger
2024-03-05 08:39:06 +01:00
parent 597477544d
commit b5dbaad9aa
23 changed files with 337 additions and 341 deletions

View File

@@ -46,7 +46,6 @@
A7FB0D7A2B25C66600A3469E /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7FB0D792B25C66600A3469E /* OnboardingView.swift */; }; A7FB0D7A2B25C66600A3469E /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7FB0D792B25C66600A3469E /* OnboardingView.swift */; };
A7FB0D7C2B25C68500A3469E /* TokenLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7FB0D7B2B25C68500A3469E /* TokenLoginView.swift */; }; A7FB0D7C2B25C68500A3469E /* TokenLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7FB0D7B2B25C68500A3469E /* TokenLoginView.swift */; };
A7FB0D7E2B25C6A200A3469E /* V2LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7FB0D7D2B25C6A200A3469E /* V2LoginView.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 */; }; A97506132B920D9F00E86029 /* RecipeDurationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97506122B920D9F00E86029 /* RecipeDurationSection.swift */; };
A97506152B920DF200E86029 /* RecipeGenericViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97506142B920DF200E86029 /* RecipeGenericViews.swift */; }; A97506152B920DF200E86029 /* RecipeGenericViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97506142B920DF200E86029 /* RecipeGenericViews.swift */; };
A97506192B920EC200E86029 /* RecipeIngredientSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97506182B920EC200E86029 /* RecipeIngredientSection.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 */; }; A977D0E22B60034E009783A9 /* GroceryListTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A977D0E12B60034E009783A9 /* GroceryListTabView.swift */; };
A97B4D322B80B3E900EC1A88 /* RecipeModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97B4D312B80B3E900EC1A88 /* RecipeModels.swift */; }; A97B4D322B80B3E900EC1A88 /* RecipeModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97B4D312B80B3E900EC1A88 /* RecipeModels.swift */; };
A97B4D352B80B82A00EC1A88 /* ShareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97B4D342B80B82A00EC1A88 /* ShareView.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 */; }; A9BBB38C2B8D3B0C002DA7FF /* ParallaxHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BBB38B2B8D3B0C002DA7FF /* ParallaxHeaderView.swift */; };
A9BBB38E2B8E44B3002DA7FF /* BottomClipper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BBB38D2B8E44B3002DA7FF /* BottomClipper.swift */; }; A9BBB38E2B8E44B3002DA7FF /* BottomClipper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BBB38D2B8E44B3002DA7FF /* BottomClipper.swift */; };
A9BBB3902B91BE31002DA7FF /* ObservableRecipeDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BBB38F2B91BE31002DA7FF /* ObservableRecipeDetail.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 = "<group>"; }; A7FB0D792B25C66600A3469E /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
A7FB0D7B2B25C68500A3469E /* TokenLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenLoginView.swift; sourceTree = "<group>"; }; A7FB0D7B2B25C68500A3469E /* TokenLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenLoginView.swift; sourceTree = "<group>"; };
A7FB0D7D2B25C6A200A3469E /* V2LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = V2LoginView.swift; sourceTree = "<group>"; }; A7FB0D7D2B25C6A200A3469E /* V2LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = V2LoginView.swift; sourceTree = "<group>"; };
A95364662B7E89F1001018B0 /* ReorderableForEach.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableForEach.swift; sourceTree = "<group>"; };
A97506122B920D9F00E86029 /* RecipeDurationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeDurationSection.swift; sourceTree = "<group>"; }; A97506122B920D9F00E86029 /* RecipeDurationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeDurationSection.swift; sourceTree = "<group>"; };
A97506142B920DF200E86029 /* RecipeGenericViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeGenericViews.swift; sourceTree = "<group>"; }; A97506142B920DF200E86029 /* RecipeGenericViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeGenericViews.swift; sourceTree = "<group>"; };
A97506182B920EC200E86029 /* RecipeIngredientSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeIngredientSection.swift; sourceTree = "<group>"; }; A97506182B920EC200E86029 /* RecipeIngredientSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeIngredientSection.swift; sourceTree = "<group>"; };
@@ -157,7 +155,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
A74D33BE2AF82AAE00D06555 /* SwiftSoup in Frameworks */, A74D33BE2AF82AAE00D06555 /* SwiftSoup in Frameworks */,
A99DC7BC2B6411A7000118AA /* SimilaritySearchKit in Frameworks */, A9A43AE12B963150003D95CA /* SwipeActions in Frameworks */,
A9CA6CF62B4C63F200F78AB5 /* TPPDF in Frameworks */, A9CA6CF62B4C63F200F78AB5 /* TPPDF in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -409,7 +407,6 @@
children = ( children = (
A9BBB38B2B8D3B0C002DA7FF /* ParallaxHeaderView.swift */, A9BBB38B2B8D3B0C002DA7FF /* ParallaxHeaderView.swift */,
A7CD3FD12B2C546A00D764AD /* CollapsibleView.swift */, A7CD3FD12B2C546A00D764AD /* CollapsibleView.swift */,
A95364662B7E89F1001018B0 /* ReorderableForEach.swift */,
A9BBB38D2B8E44B3002DA7FF /* BottomClipper.swift */, A9BBB38D2B8E44B3002DA7FF /* BottomClipper.swift */,
); );
path = ReusableViews; path = ReusableViews;
@@ -450,7 +447,7 @@
packageProductDependencies = ( packageProductDependencies = (
A74D33BD2AF82AAE00D06555 /* SwiftSoup */, A74D33BD2AF82AAE00D06555 /* SwiftSoup */,
A9CA6CF52B4C63F200F78AB5 /* TPPDF */, A9CA6CF52B4C63F200F78AB5 /* TPPDF */,
A99DC7BB2B6411A7000118AA /* SimilaritySearchKit */, A9A43AE02B963150003D95CA /* SwipeActions */,
); );
productName = "Nextcloud Cookbook iOS Client"; productName = "Nextcloud Cookbook iOS Client";
productReference = A701717E2AA8E71900064C43 /* Nextcloud Cookbook iOS Client.app */; productReference = A701717E2AA8E71900064C43 /* Nextcloud Cookbook iOS Client.app */;
@@ -530,7 +527,7 @@
packageReferences = ( packageReferences = (
A74D33BC2AF82AAE00D06555 /* XCRemoteSwiftPackageReference "SwiftSoup" */, A74D33BC2AF82AAE00D06555 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
A9CA6CF42B4C63F200F78AB5 /* XCRemoteSwiftPackageReference "TPPDF" */, A9CA6CF42B4C63F200F78AB5 /* XCRemoteSwiftPackageReference "TPPDF" */,
A99DC7BA2B6411A7000118AA /* XCRemoteSwiftPackageReference "similarity-search-kit" */, A9A43ADF2B963150003D95CA /* XCRemoteSwiftPackageReference "SwipeActions" */,
); );
productRefGroup = A701717F2AA8E71900064C43 /* Products */; productRefGroup = A701717F2AA8E71900064C43 /* Products */;
projectDirPath = ""; projectDirPath = "";
@@ -594,7 +591,6 @@
A70171C42AB4A31200064C43 /* DataStore.swift in Sources */, A70171C42AB4A31200064C43 /* DataStore.swift in Sources */,
A7FB0D7E2B25C6A200A3469E /* V2LoginView.swift in Sources */, A7FB0D7E2B25C6A200A3469E /* V2LoginView.swift in Sources */,
A975061D2B920FCC00E86029 /* RecipeInstructionSection.swift in Sources */, A975061D2B920FCC00E86029 /* RecipeInstructionSection.swift in Sources */,
A95364672B7E89F1001018B0 /* ReorderableForEach.swift in Sources */,
A787B0782B2B1E6400C2DF1B /* DateExtension.swift in Sources */, A787B0782B2B1E6400C2DF1B /* DateExtension.swift in Sources */,
A79AA8E92B062DD1007D25F2 /* CookbookApiV1.swift in Sources */, A79AA8E92B062DD1007D25F2 /* CookbookApiV1.swift in Sources */,
A7CD3FD22B2C546A00D764AD /* CollapsibleView.swift in Sources */, A7CD3FD22B2C546A00D764AD /* CollapsibleView.swift in Sources */,
@@ -1009,12 +1005,12 @@
minimumVersion = 2.6.1; minimumVersion = 2.6.1;
}; };
}; };
A99DC7BA2B6411A7000118AA /* XCRemoteSwiftPackageReference "similarity-search-kit" */ = { A9A43ADF2B963150003D95CA /* XCRemoteSwiftPackageReference "SwipeActions" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/ZachNagengast/similarity-search-kit"; repositoryURL = "https://github.com/aheze/SwipeActions";
requirement = { requirement = {
kind = upToNextMajorVersion; kind = upToNextMajorVersion;
minimumVersion = 0.0.13; minimumVersion = 1.1.0;
}; };
}; };
A9CA6CF42B4C63F200F78AB5 /* XCRemoteSwiftPackageReference "TPPDF" */ = { A9CA6CF42B4C63F200F78AB5 /* XCRemoteSwiftPackageReference "TPPDF" */ = {
@@ -1033,10 +1029,10 @@
package = A74D33BC2AF82AAE00D06555 /* XCRemoteSwiftPackageReference "SwiftSoup" */; package = A74D33BC2AF82AAE00D06555 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
productName = SwiftSoup; productName = SwiftSoup;
}; };
A99DC7BB2B6411A7000118AA /* SimilaritySearchKit */ = { A9A43AE02B963150003D95CA /* SwipeActions */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = A99DC7BA2B6411A7000118AA /* XCRemoteSwiftPackageReference "similarity-search-kit" */; package = A9A43ADF2B963150003D95CA /* XCRemoteSwiftPackageReference "SwipeActions" */;
productName = SimilaritySearchKit; productName = SwipeActions;
}; };
A9CA6CF52B4C63F200F78AB5 /* TPPDF */ = { A9CA6CF52B4C63F200F78AB5 /* TPPDF */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View File

@@ -1,14 +1,5 @@
{ {
"pins" : [ "pins" : [
{
"identity" : "similarity-search-kit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ZachNagengast/similarity-search-kit",
"state" : {
"revision" : "ddc8e458d0e826b2fe5dbce6f6eac96a8935e8eb",
"version" : "0.0.13"
}
},
{ {
"identity" : "swiftsoup", "identity" : "swiftsoup",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@@ -18,6 +9,15 @@
"version" : "2.6.1" "version" : "2.6.1"
} }
}, },
{
"identity" : "swipeactions",
"kind" : "remoteSourceControl",
"location" : "https://github.com/aheze/SwipeActions",
"state" : {
"revision" : "41e6f6dce02d8cfa164f8c5461a41340850ca3ab",
"version" : "1.1.0"
}
},
{ {
"identity" : "tppdf", "identity" : "tppdf",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildLocationStyle</key>
<string>UseAppPreferences</string>
<key>CustomBuildLocationType</key>
<string>RelativeToDerivedData</string>
<key>DerivedDataLocationStyle</key>
<string>Default</string>
<key>ShowSharedSchemesAutomaticallyEnabled</key>
<true/>
</dict>
</plist>

View File

@@ -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")
}
}
}

View File

@@ -20,9 +20,9 @@ class ObservableRecipeDetail: ObservableObject {
@Published var url: String @Published var url: String
@Published var recipeYield: Int @Published var recipeYield: Int
@Published var recipeCategory: String @Published var recipeCategory: String
@Published var tool: [ReorderableItem<String>] @Published var tool: [String]
@Published var recipeIngredient: [ReorderableItem<String>] @Published var recipeIngredient: [String]
@Published var recipeInstructions: [ReorderableItem<String>] @Published var recipeInstructions: [String]
@Published var nutrition: [String:String] @Published var nutrition: [String:String]
init() { init() {
@@ -55,9 +55,9 @@ class ObservableRecipeDetail: ObservableObject {
url = recipeDetail.url url = recipeDetail.url
recipeYield = recipeDetail.recipeYield recipeYield = recipeDetail.recipeYield
recipeCategory = recipeDetail.recipeCategory recipeCategory = recipeDetail.recipeCategory
tool = ReorderableItem.list(items: recipeDetail.tool) tool = recipeDetail.tool
recipeIngredient = ReorderableItem.list(items: recipeDetail.recipeIngredient) recipeIngredient = recipeDetail.recipeIngredient
recipeInstructions = ReorderableItem.list(items: recipeDetail.recipeInstructions) recipeInstructions = recipeDetail.recipeInstructions
nutrition = recipeDetail.nutrition nutrition = recipeDetail.nutrition
} }
@@ -76,9 +76,9 @@ class ObservableRecipeDetail: ObservableObject {
url: self.url, url: self.url,
recipeYield: self.recipeYield, recipeYield: self.recipeYield,
recipeCategory: self.recipeCategory, recipeCategory: self.recipeCategory,
tool: ReorderableItem.items(self.tool), tool: self.tool,
recipeIngredient: ReorderableItem.items(self.recipeIngredient), recipeIngredient: self.recipeIngredient,
recipeInstructions: ReorderableItem.items(self.recipeInstructions), recipeInstructions: self.recipeInstructions,
nutrition: self.nutrition nutrition: self.nutrition
) )
} }

View File

@@ -12,6 +12,9 @@ extension Color {
public static var nextcloudBlue: Color { public static var nextcloudBlue: Color {
return Color("ncblue") return Color("ncblue")
} }
public static var nextcloudDarkBlue: Color {
return Color("ncdarkblue")
}
public static var backgroundHighlight: Color { public static var backgroundHighlight: Color {
return Color("backgroundHighlight") return Color("backgroundHighlight")
} }

View File

@@ -230,6 +230,7 @@
}, },
"%lld." : { "%lld." : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
@@ -407,6 +408,7 @@
} }
}, },
"Add" : { "Add" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"de" : { "de" : {
"stringUnit" : { "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." : { "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" : { "localizations" : {
@@ -634,6 +639,7 @@
} }
}, },
"Category: %@" : { "Category: %@" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
@@ -816,6 +822,7 @@
} }
}, },
"Cooking duration:" : { "Cooking duration:" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
@@ -1056,9 +1063,6 @@
} }
} }
} }
},
"Disable deletion" : {
}, },
"Done" : { "Done" : {
"localizations" : { "localizations" : {
@@ -1180,9 +1184,6 @@
} }
} }
} }
},
"Enable deletion" : {
}, },
"Error" : { "Error" : {
"localizations" : { "localizations" : {
@@ -1525,6 +1526,7 @@
} }
}, },
"Import recipe from a website" : { "Import recipe from a website" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
@@ -1755,6 +1757,9 @@
} }
} }
} }
},
"List your tools here. 🍴" : {
}, },
"Log out" : { "Log out" : {
"localizations" : { "localizations" : {
@@ -1982,6 +1987,7 @@
} }
}, },
"New recipe" : { "New recipe" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
@@ -2359,6 +2365,7 @@
} }
}, },
"Preparation duration:" : { "Preparation duration:" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
@@ -2564,9 +2571,6 @@
} }
} }
} }
},
"Select Item" : {
}, },
"Select Keywords" : { "Select Keywords" : {
@@ -2600,6 +2604,7 @@
}, },
"Servings:" : { "Servings:" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
@@ -2733,6 +2738,9 @@
}, },
"Sodium content" : { "Sodium content" : {
"comment" : "Sodium content" "comment" : "Sodium content"
},
"Start by adding your first ingredient! 🥬" : {
}, },
"Store recipe images locally" : { "Store recipe images locally" : {
"localizations" : { "localizations" : {
@@ -3026,6 +3034,7 @@
} }
}, },
"Title" : { "Title" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
@@ -3071,11 +3080,9 @@
} }
} }
} }
},
"Total" : {
}, },
"Total duration:" : { "Total duration:" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
@@ -3236,6 +3243,7 @@
"comment" : "Unsaturated fat content" "comment" : "Unsaturated fat content"
}, },
"Upload" : { "Upload" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {

View File

@@ -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
}
} }

View File

@@ -6,7 +6,6 @@
// //
import SwiftUI import SwiftUI
import SimilaritySearchKit
struct MainView: View { struct MainView: View {
@StateObject var appState = AppState() @StateObject var appState = AppState()

View File

@@ -9,7 +9,7 @@ import Foundation
import SwiftUI import SwiftUI
/*
struct CategoryPickerViewOld: View { struct CategoryPickerViewOld: View {
@State var title: String @State var title: String
@State var searchSuggestions: [String] @State var searchSuggestions: [String]
@@ -61,3 +61,4 @@ struct CategoryPickerViewOld: View {
} }
} }
*/

View File

@@ -10,7 +10,7 @@ import SwiftUI
import PhotosUI import PhotosUI
/*
struct RecipeEditView: View { struct RecipeEditView: View {
@ObservedObject var viewModel: RecipeEditViewModel @ObservedObject var viewModel: RecipeEditViewModel
@Binding var isPresented: Bool @Binding var isPresented: Bool
@@ -279,4 +279,4 @@ fileprivate struct DurationPicker: View {
*/

View File

@@ -34,6 +34,16 @@ struct RecipeView: View {
.scaledToFill() .scaledToFill()
.frame(maxHeight: imageHeight + 200) .frame(maxHeight: imageHeight + 200)
.clipped() .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 { if viewModel.editMode {
RecipeImportSection(viewModel: viewModel, importRecipe: importRecipe) RecipeImportSection(viewModel: viewModel, importRecipe: importRecipe)
} }
if viewModel.editMode {
RecipeMetadataSection(viewModel: viewModel)
}
HStack { HStack {
EditableText(text: $viewModel.observableRecipeDetail.name, editMode: $viewModel.editMode, titleKey: "Recipe Name") EditableText(text: $viewModel.observableRecipeDetail.name, editMode: $viewModel.editMode, titleKey: "Recipe Name")
.font(.title) .font(.title)
@@ -66,10 +81,6 @@ struct RecipeView: View {
Divider() Divider()
if viewModel.editMode {
RecipeMetadataSection(viewModel: viewModel)
}
LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) { LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) {
if(!viewModel.observableRecipeDetail.recipeIngredient.isEmpty || viewModel.editMode) { if(!viewModel.observableRecipeDetail.recipeIngredient.isEmpty || viewModel.editMode) {
RecipeIngredientSection(viewModel: viewModel) RecipeIngredientSection(viewModel: viewModel)
@@ -83,13 +94,12 @@ struct RecipeView: View {
RecipeNutritionSection(viewModel: viewModel) RecipeNutritionSection(viewModel: viewModel)
} }
Divider() if !viewModel.editMode {
Divider()
LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) { LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) {
if !viewModel.editMode {
RecipeKeywordSection(viewModel: viewModel) RecipeKeywordSection(viewModel: viewModel)
MoreInformationSection(viewModel: viewModel)
} }
MoreInformationSection(viewModel: viewModel)
} }
} }
.padding(.horizontal, 5) .padding(.horizontal, 5)
@@ -99,6 +109,7 @@ struct RecipeView: View {
.coordinateSpace(name: CoordinateSpaces.scrollView) .coordinateSpace(name: CoordinateSpaces.scrollView)
.ignoresSafeArea(.container, edges: .top) .ignoresSafeArea(.container, edges: .top)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
//.toolbarTitleDisplayMode(.inline)
.navigationTitle(viewModel.showTitle ? viewModel.recipe.name : "") .navigationTitle(viewModel.showTitle ? viewModel.recipe.name : "")
.toolbar { .toolbar {
if viewModel.editMode { if viewModel.editMode {
@@ -112,7 +123,6 @@ struct RecipeView: View {
// Upload Button // Upload Button
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
Button { Button {
// TODO: POST edited recipe
Task { Task {
if viewModel.newRecipe { if viewModel.newRecipe {
if let res = await uploadNewRecipe() { if let res = await uploadNewRecipe() {
@@ -191,6 +201,7 @@ struct RecipeView: View {
} label: { } label: {
Image(systemName: "ellipsis.circle") Image(systemName: "ellipsis.circle")
} }
} }
} }
} }
@@ -199,6 +210,36 @@ struct RecipeView: View {
recipeImage: viewModel.recipeImage, recipeImage: viewModel.recipeImage,
presentShareSheet: $viewModel.presentShareSheet) 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 { .task {
// Load recipe detail // Load recipe detail
@@ -282,10 +323,15 @@ struct RecipeView: View {
@Published var recipeDetail: RecipeDetail = RecipeDetail.error @Published var recipeDetail: RecipeDetail = RecipeDetail.error
@Published var recipeImage: UIImage? = nil @Published var recipeImage: UIImage? = nil
@Published var editMode: Bool = false @Published var editMode: Bool = false
@Published var presentShareSheet: Bool = false
@Published var showTitle: Bool = false @Published var showTitle: Bool = false
@Published var isDownloaded: Bool? = nil @Published var isDownloaded: Bool? = nil
@Published var importUrl: String = "" @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 recipe: Recipe
var sharedURL: URL? = nil var sharedURL: URL? = nil
var newRecipe: Bool = false var newRecipe: Bool = false
@@ -408,26 +454,28 @@ fileprivate struct RecipeImportSection: View {
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
HStack {
TextField(LocalizedStringKey("URL (e.g. example.com/recipe)"), text: $viewModel.importUrl) TextField(LocalizedStringKey("URL (e.g. example.com/recipe)"), text: $viewModel.importUrl)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
Button { .padding(.top, 5)
Task { Button {
if let res = await importRecipe(viewModel.importUrl) { Task {
viewModel.alertType = RecipeAlert.CUSTOM( if let res = await importRecipe(viewModel.importUrl) {
title: res.localizedTitle, viewModel.alertType = RecipeAlert.CUSTOM(
description: res.localizedDescription title: res.localizedTitle,
) description: res.localizedDescription
viewModel.alertAction = { } )
viewModel.presentAlert = true viewModel.alertAction = { }
} viewModel.presentAlert = true
} }
} label: {
Text(LocalizedStringKey("Import"))
} }
}.padding(.top, 5) } label: {
Text(LocalizedStringKey("Import"))
}
.buttonStyle(.bordered)
} }
.padding() .padding()
.background(Rectangle().foregroundStyle(Color.white.opacity(0.1))) .background(RoundedRectangle(cornerRadius: 20).foregroundStyle(Color.white.opacity(0.1)))
.padding()
} }
} }

View File

@@ -15,40 +15,28 @@ struct RecipeDurationSection: View {
@State var presentPopover: Bool = false @State var presentPopover: Bool = false
var body: some View { var body: some View {
if !viewModel.editMode { VStack(alignment: .leading) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200, maximum: .infinity), alignment: .leading)]) { LazyVGrid(columns: [GridItem(.adaptive(minimum: 200, maximum: .infinity), alignment: .leading)]) {
DurationView(time: viewModel.observableRecipeDetail.prepTime, title: LocalizedStringKey("Preparation")) DurationView(time: viewModel.observableRecipeDetail.prepTime, title: LocalizedStringKey("Preparation"))
DurationView(time: viewModel.observableRecipeDetail.cookTime, title: LocalizedStringKey("Cooking")) DurationView(time: viewModel.observableRecipeDetail.cookTime, title: LocalizedStringKey("Cooking"))
DurationView(time: viewModel.observableRecipeDetail.totalTime, title: LocalizedStringKey("Total time")) DurationView(time: viewModel.observableRecipeDetail.totalTime, title: LocalizedStringKey("Total time"))
} }
} else { Button {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200, maximum: .infinity), alignment: .leading)]) { presentPopover.toggle()
Button { } label: {
presentPopover.toggle() Text("Edit")
} 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
)
} }
.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 { HStack {
Image(systemName: "clock") Image(systemName: "clock")
.bold()
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
Text(time.displayString) Text(time.displayString)
.lineLimit(1) .lineLimit(1)
} }
} }
.padding()
} }
} }
fileprivate struct EditableDurationView: View { fileprivate struct EditableDurationView: View {
@Environment(\.presentationMode) var presentationMode
@ObservedObject var prepTime: DurationComponents @ObservedObject var prepTime: DurationComponents
@ObservedObject var cookTime: DurationComponents @ObservedObject var cookTime: DurationComponents
@ObservedObject var totalTime: DurationComponents @ObservedObject var totalTime: DurationComponents
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(alignment: .leading) { VStack(alignment: .center) {
HStack { HStack {
SecondaryLabel(text: "Preparation") SecondaryLabel(text: "Preparation")
Spacer() Spacer()
Button("Done") {
presentationMode.wrappedValue.dismiss()
}
} }
TimePickerView(selectedHour: $prepTime.hourComponent, selectedMinute: $prepTime.minuteComponent) TimePickerView(selectedHour: $prepTime.hourComponent, selectedMinute: $prepTime.minuteComponent)
SecondaryLabel(text: "Cooking")
HStack {
SecondaryLabel(text: "Cooking")
Spacer()
}
TimePickerView(selectedHour: $cookTime.hourComponent, selectedMinute: $cookTime.minuteComponent) TimePickerView(selectedHour: $cookTime.hourComponent, selectedMinute: $cookTime.minuteComponent)
SecondaryLabel(text: "Total")
HStack {
SecondaryLabel(text: "Total time")
Spacer()
}
TimePickerView(selectedHour: $totalTime.hourComponent, selectedMinute: $totalTime.minuteComponent) TimePickerView(selectedHour: $totalTime.hourComponent, selectedMinute: $totalTime.minuteComponent)
} }
.padding() .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
}
} }

View File

@@ -59,27 +59,93 @@ struct EditableText: View {
} }
struct EditableStringList<Content: View>: View { struct EditableListView: View {
@Binding var items: [ReorderableItem<String>] @Binding var isPresented: Bool
@Binding var editMode: Bool @Binding var items: [String]
@State var title: LocalizedStringKey
@State var emptyListText: LocalizedStringKey
@State var titleKey: LocalizedStringKey = "" @State var titleKey: LocalizedStringKey = ""
@State var lineLimit: ClosedRange<Int> = 0...50 @State var lineLimit: ClosedRange<Int> = 0...50
@State var axis: Axis = .vertical @State var axis: Axis = .vertical
var content: () -> Content
var body: some View { var body: some View {
if editMode { NavigationView {
VStack { ZStack {
ReorderableForEach(items: $items, defaultItem: ReorderableItem(item: "")) { ix, item in List {
TextField("", text: $items[ix].item, axis: axis) if items.isEmpty {
.textFieldStyle(.roundedBorder) Text(emptyListText)
.lineLimit(lineLimit) }
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) .navigationBarTitle(title, displayMode: .inline)
} else { .navigationBarItems(
content() 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.")
})
}
}

View File

@@ -31,7 +31,7 @@ struct RecipeIngredientSection: View {
groceryList.deleteGroceryRecipe(viewModel.observableRecipeDetail.id) groceryList.deleteGroceryRecipe(viewModel.observableRecipeDetail.id)
} else { } else {
groceryList.addItems( groceryList.addItems(
ReorderableItem.items(viewModel.observableRecipeDetail.recipeIngredient), viewModel.observableRecipeDetail.recipeIngredient,
toRecipe: viewModel.observableRecipeDetail.id, toRecipe: viewModel.observableRecipeDetail.id,
recipeName: viewModel.observableRecipeDetail.name 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..<viewModel.observableRecipeDetail.recipeIngredient.count, id: \.self) { ix in
ForEach(0..<viewModel.observableRecipeDetail.recipeIngredient.count, id: \.self) { ix in IngredientListItem(ingredient: $viewModel.observableRecipeDetail.recipeIngredient[ix], recipeId: viewModel.observableRecipeDetail.id) {
IngredientListItem(ingredient: viewModel.observableRecipeDetail.recipeIngredient[ix], recipeId: viewModel.observableRecipeDetail.id) { groceryList.addItem(
groceryList.addItem( viewModel.observableRecipeDetail.recipeIngredient[ix],
viewModel.observableRecipeDetail.recipeIngredient[ix].item, toRecipe: viewModel.observableRecipeDetail.id,
toRecipe: viewModel.observableRecipeDetail.id, recipeName: viewModel.observableRecipeDetail.name
recipeName: viewModel.observableRecipeDetail.name )
)
}
.padding(4)
} }
.padding(4)
}
if viewModel.editMode {
Button {
viewModel.presentIngredientEditView.toggle()
} label: {
Text("Edit")
}
.buttonStyle(.borderedProminent)
} }
}.padding() }.padding()
} }
@@ -66,7 +72,7 @@ struct RecipeIngredientSection: View {
fileprivate struct IngredientListItem: View { fileprivate struct IngredientListItem: View {
@EnvironmentObject var groceryList: GroceryList @EnvironmentObject var groceryList: GroceryList
@State var ingredient: ReorderableItem<String> @Binding var ingredient: String
@State var recipeId: String @State var recipeId: String
let addToGroceryListAction: () -> Void let addToGroceryListAction: () -> Void
@State var isSelected: Bool = false @State var isSelected: Bool = false
@@ -78,7 +84,7 @@ fileprivate struct IngredientListItem: View {
var body: some View { var body: some View {
HStack(alignment: .top) { HStack(alignment: .top) {
if groceryList.containsItem(at: recipeId, item: ingredient.item) { if groceryList.containsItem(at: recipeId, item: ingredient) {
if #available(iOS 17.0, *) { if #available(iOS 17.0, *) {
Image(systemName: "storefront") Image(systemName: "storefront")
.foregroundStyle(Color.green) .foregroundStyle(Color.green)
@@ -93,7 +99,7 @@ fileprivate struct IngredientListItem: View {
Image(systemName: "circle") Image(systemName: "circle")
} }
Text("\(ingredient.item)") Text("\(ingredient)")
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
.lineLimit(5) .lineLimit(5)
Spacer() Spacer()
@@ -119,15 +125,13 @@ fileprivate struct IngredientListItem: View {
.onEnded { gesture in .onEnded { gesture in
withAnimation { withAnimation {
if dragOffset > maxDragDistance * 0.3 { // Swipe threshold if dragOffset > maxDragDistance * 0.3 { // Swipe threshold
if groceryList.containsItem(at: recipeId, item: ingredient.item) { if groceryList.containsItem(at: recipeId, item: ingredient) {
groceryList.deleteItem(ingredient.item, fromRecipe: recipeId) groceryList.deleteItem(ingredient, fromRecipe: recipeId)
} else { } else {
addToGroceryListAction() addToGroceryListAction()
} }
} }
// Animate back to original position // Animate back to original position
self.dragOffset = 0 self.dragOffset = 0
self.animationStartOffset = 0 self.animationStartOffset = 0
} }

View File

@@ -19,19 +19,27 @@ struct RecipeInstructionSection: View {
SecondaryLabel(text: LocalizedStringKey("Instructions")) SecondaryLabel(text: LocalizedStringKey("Instructions"))
Spacer() Spacer()
} }
EditableStringList(items: $viewModel.observableRecipeDetail.recipeInstructions, editMode: $viewModel.editMode, titleKey: "Instruction", lineLimit: 0...15, axis: .vertical) { ForEach(viewModel.observableRecipeDetail.recipeInstructions.indices, id: \.self) { ix in
ForEach(0..<viewModel.observableRecipeDetail.recipeInstructions.count, id: \.self) { ix in RecipeInstructionListItem(instruction: $viewModel.observableRecipeDetail.recipeInstructions[ix], index: ix+1)
RecipeInstructionListItem(instruction: viewModel.observableRecipeDetail.recipeInstructions[ix], index: ix+1)
}
} }
}.padding() if viewModel.editMode {
Button {
viewModel.presentInstructionEditView.toggle()
} label: {
Text("Edit")
}
.buttonStyle(.borderedProminent)
}
}
.padding()
} }
} }
fileprivate struct RecipeInstructionListItem: View { fileprivate struct RecipeInstructionListItem: View {
@State var instruction: ReorderableItem<String> @Binding var instruction: String
@State var index: Int @State var index: Int
@State var isSelected: Bool = false @State var isSelected: Bool = false
@@ -39,7 +47,7 @@ fileprivate struct RecipeInstructionListItem: View {
HStack(alignment: .top) { HStack(alignment: .top) {
Text("\(index)") Text("\(index)")
.monospaced() .monospaced()
Text(instruction.item) Text(instruction)
}.padding(4) }.padding(4)
.foregroundStyle(isSelected ? Color.secondary : Color.primary) .foregroundStyle(isSelected ? Color.secondary : Color.primary)
.onTapGesture { .onTapGesture {

View File

@@ -19,7 +19,6 @@ struct RecipeMetadataSection: View {
appState.categories.map({ category in category.name }) appState.categories.map({ category in category.name })
} }
@State var presentKeywordSheet: Bool = false @State var presentKeywordSheet: Bool = false
@State var presentServingsPopover: Bool = false @State var presentServingsPopover: Bool = false
@State var presentCategoryPopover: Bool = false @State var presentCategoryPopover: Bool = false
@@ -27,7 +26,6 @@ struct RecipeMetadataSection: View {
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
// Category // Category
//CategoryPickerView(items: $categories, input: $viewModel.observableRecipeDetail.recipeCategory, titleKey: "Category")
SecondaryLabel(text: "Category") SecondaryLabel(text: "Category")
HStack { HStack {
TextField("Category", text: $viewModel.observableRecipeDetail.recipeCategory) TextField("Category", text: $viewModel.observableRecipeDetail.recipeCategory)
@@ -42,6 +40,7 @@ struct RecipeMetadataSection: View {
} }
.pickerStyle(.menu) .pickerStyle(.menu)
} }
.padding(.bottom)
// Keywords // Keywords
SecondaryLabel(text: "Keywords") SecondaryLabel(text: "Keywords")
@@ -51,6 +50,8 @@ struct RecipeMetadataSection: View {
HStack { HStack {
ForEach(viewModel.observableRecipeDetail.keywords, id: \.self) { keyword in ForEach(viewModel.observableRecipeDetail.keywords, id: \.self) { keyword in
Text(keyword) Text(keyword)
.padding(5)
.background(RoundedRectangle(cornerRadius: 20).foregroundStyle(Color.primary.opacity(0.1)))
} }
} }
} }
@@ -61,6 +62,7 @@ struct RecipeMetadataSection: View {
Text("Select Keywords") Text("Select Keywords")
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
} }
.padding(.bottom)
// Servings / Yield // Servings / Yield
VStack(alignment: .leading) { VStack(alignment: .leading) {
@@ -77,7 +79,8 @@ struct RecipeMetadataSection: View {
} }
} }
.padding() .padding()
.background(Rectangle().foregroundStyle(Color.white.opacity(0.1))) .background(RoundedRectangle(cornerRadius: 20).foregroundStyle(Color.white.opacity(0.1)))
.padding()
.sheet(isPresented: $presentKeywordSheet) { .sheet(isPresented: $presentKeywordSheet) {
KeywordPickerView(title: "Keywords", searchSuggestions: appState.allKeywords, selection: $viewModel.observableRecipeDetail.keywords) KeywordPickerView(title: "Keywords", searchSuggestions: appState.allKeywords, selection: $viewModel.observableRecipeDetail.keywords)
} }
@@ -104,47 +107,6 @@ fileprivate struct PickerPopoverView<Item: Hashable & CustomStringConvertible, C
} }
} }
fileprivate struct CategoryPickerView: View {
@Binding var items: [String]
@Binding var input: String
@State private var pickerChoice: String = ""
var titleKey: LocalizedStringKey
var body: some View {
VStack(alignment: .leading) {
SecondaryLabel(text: "Category")
.padding([.top, .horizontal])
HStack {
TextField(titleKey, text: $input)
.lineLimit(1)
.textFieldStyle(.roundedBorder)
.padding()
.onSubmit {
pickerChoice = ""
}
Picker("Select Item", selection: $pickerChoice) {
Text("").tag("")
ForEach(items, id: \.self) { item in
Text(item)
}
}
.pickerStyle(.menu)
.padding()
.onChange(of: pickerChoice) { newValue in
if pickerChoice != "" {
input = newValue
}
}
}
}
.onAppear {
pickerChoice = input
}
}
}
// MARK: - RecipeView More Information Section // MARK: - RecipeView More Information Section

View File

@@ -19,9 +19,19 @@ struct RecipeToolSection: View {
SecondaryLabel(text: "Tools") SecondaryLabel(text: "Tools")
Spacer() Spacer()
} }
EditableStringList(items: $viewModel.observableRecipeDetail.tool, editMode: $viewModel.editMode, titleKey: "Tool", lineLimit: 0...1, axis: .horizontal) {
RecipeListSection(list: ReorderableItem.items(viewModel.observableRecipeDetail.tool)) RecipeListSection(list: viewModel.observableRecipeDetail.tool)
if viewModel.editMode {
Button {
viewModel.presentToolEditView.toggle()
} label: {
Text("Edit")
}
.buttonStyle(.borderedProminent)
} }
}.padding() }.padding()
} }
} }

View File

@@ -1,116 +0,0 @@
//
// ReorderableForEach.swift
// Nextcloud Cookbook iOS Client
//
// Created by Vincent Meilinger on 15.02.24.
//
import Foundation
import SwiftUI
import UniformTypeIdentifiers
struct ReorderableForEach<Item: Any, Content: View>: View {
@Binding var items: [ReorderableItem<Item>]
var defaultItem: ReorderableItem<Item>
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<Item: Any>: 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<Item: Any>: DropDelegate {
let targetId: UUID
@Binding var sourceId : UUID?
@Binding var items: [ReorderableItem<Item>]
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)
}
}
}
}

View File

@@ -65,17 +65,6 @@ struct RecipeTabView: View {
} }
} }
.tint(.nextcloudBlue) .tint(.nextcloudBlue)
/*.sheet(isPresented: $viewModel.presentEditView) {
RecipeEditView(
viewModel:
RecipeEditViewModel(
mainViewModel: mainViewModel,
uploadNew: true
),
isPresented: $viewModel.presentEditView
)
}*/
.task { .task {
viewModel.serverConnection = await mainViewModel.checkServerConnection() viewModel.serverConnection = await mainViewModel.checkServerConnection()
} }

View File

@@ -7,7 +7,6 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import SimilaritySearchKit
struct SearchTabView: View { struct SearchTabView: View {
@EnvironmentObject var viewModel: SearchTabView.ViewModel @EnvironmentObject var viewModel: SearchTabView.ViewModel
@@ -56,8 +55,7 @@ struct SearchTabView: View {
@Published var searchText: String = "" @Published var searchText: String = ""
@Published var searchMode: SearchMode = .name @Published var searchMode: SearchMode = .name
var similarityIndex: SimilarityIndex? = nil
var similaritySearchResults: [SearchResult] = []
enum SearchMode: String, CaseIterable { enum SearchMode: String, CaseIterable {
case name = "Name & Keywords", ingredient = "Ingredients" case name = "Name & Keywords", ingredient = "Ingredients"