Recipes are now searchable

This commit is contained in:
Vicnet
2023-10-03 12:11:32 +02:00
parent ee1c0d9aed
commit 77c07bb0b1
48 changed files with 297 additions and 86 deletions

0
Icon Normal file
View File

View File

@@ -33,6 +33,7 @@
A703226F2ABB1DD700D7C4ED /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A703226E2ABB1DD700D7C4ED /* ColorExtension.swift */; }; A703226F2ABB1DD700D7C4ED /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A703226E2ABB1DD700D7C4ED /* ColorExtension.swift */; };
A70D7CA12AC73CA800D53DBF /* RecipeEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70D7CA02AC73CA700D53DBF /* RecipeEditView.swift */; }; A70D7CA12AC73CA800D53DBF /* RecipeEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70D7CA02AC73CA700D53DBF /* RecipeEditView.swift */; };
A70D7CA32AC74B3B00D53DBF /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70D7CA22AC74B3B00D53DBF /* DateExtension.swift */; }; A70D7CA32AC74B3B00D53DBF /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70D7CA22AC74B3B00D53DBF /* DateExtension.swift */; };
A7F3F8E82ACBFC760076C227 /* KeywordPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F3F8E72ACBFC760076C227 /* KeywordPickerView.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -83,6 +84,7 @@
A703226E2ABB1DD700D7C4ED /* ColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtension.swift; sourceTree = "<group>"; }; A703226E2ABB1DD700D7C4ED /* ColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtension.swift; sourceTree = "<group>"; };
A70D7CA02AC73CA700D53DBF /* RecipeEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeEditView.swift; sourceTree = "<group>"; }; A70D7CA02AC73CA700D53DBF /* RecipeEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeEditView.swift; sourceTree = "<group>"; };
A70D7CA22AC74B3B00D53DBF /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = "<group>"; }; A70D7CA22AC74B3B00D53DBF /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = "<group>"; };
A7F3F8E72ACBFC760076C227 /* KeywordPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordPickerView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -201,6 +203,7 @@
A70D7CA02AC73CA700D53DBF /* RecipeEditView.swift */, A70D7CA02AC73CA700D53DBF /* RecipeEditView.swift */,
A70171C82AB4CBB400064C43 /* OnboardingView.swift */, A70171C82AB4CBB400064C43 /* OnboardingView.swift */,
A70171CC2AB501B100064C43 /* SettingsView.swift */, A70171CC2AB501B100064C43 /* SettingsView.swift */,
A7F3F8E72ACBFC760076C227 /* KeywordPickerView.swift */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -366,6 +369,7 @@
A70171BC2AB4983500064C43 /* CategoryCardView.swift in Sources */, A70171BC2AB4983500064C43 /* CategoryCardView.swift in Sources */,
A70171BE2AB4987900064C43 /* CategoryDetailView.swift in Sources */, A70171BE2AB4987900064C43 /* CategoryDetailView.swift in Sources */,
A70171C62AB4C43A00064C43 /* DataModels.swift in Sources */, A70171C62AB4C43A00064C43 /* DataModels.swift in Sources */,
A7F3F8E82ACBFC760076C227 /* KeywordPickerView.swift in Sources */,
A70171C02AB498A900064C43 /* RecipeDetailView.swift in Sources */, A70171C02AB498A900064C43 /* RecipeDetailView.swift in Sources */,
A70171CD2AB501B100064C43 /* SettingsView.swift in Sources */, A70171CD2AB501B100064C43 /* SettingsView.swift in Sources */,
A70171C22AB498C600064C43 /* RecipeCardView.swift in Sources */, A70171C22AB498C600064C43 /* RecipeCardView.swift in Sources */,
@@ -552,7 +556,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0.1; MARKETING_VERSION = 1.0.2;
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client"; PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto; SDKROOT = auto;
@@ -593,7 +597,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0.1; MARKETING_VERSION = 1.0.2;
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client"; PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto; SDKROOT = auto;

View File

@@ -1,109 +1,109 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "cookbook-20@2x.png", "filename" : "cookbook-icon-20@2x.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "2x", "scale" : "2x",
"size" : "20x20" "size" : "20x20"
}, },
{ {
"filename" : "cookbook-20@3x.png", "filename" : "cookbook-icon-20@3x.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "3x", "scale" : "3x",
"size" : "20x20" "size" : "20x20"
}, },
{ {
"filename" : "cookbook-29@2x.png", "filename" : "cookbook-icon-29@2x.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "2x", "scale" : "2x",
"size" : "29x29" "size" : "29x29"
}, },
{ {
"filename" : "cookbook-29@3x.png", "filename" : "cookbook-icon-29@3x.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "3x", "scale" : "3x",
"size" : "29x29" "size" : "29x29"
}, },
{ {
"filename" : "cookbook-40@2x.png", "filename" : "cookbook-icon-40@2x.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "2x", "scale" : "2x",
"size" : "40x40" "size" : "40x40"
}, },
{ {
"filename" : "cookbook-40@3x.png", "filename" : "cookbook-icon-40@3x.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "3x", "scale" : "3x",
"size" : "40x40" "size" : "40x40"
}, },
{ {
"filename" : "cookbook-60@2x.png", "filename" : "cookbook-icon-60@2x.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "2x", "scale" : "2x",
"size" : "60x60" "size" : "60x60"
}, },
{ {
"filename" : "cookbook-60@3x.png", "filename" : "cookbook-icon-60@3x.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "3x", "scale" : "3x",
"size" : "60x60" "size" : "60x60"
}, },
{ {
"filename" : "cookbook-20.png", "filename" : "cookbook-icon-20.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "1x", "scale" : "1x",
"size" : "20x20" "size" : "20x20"
}, },
{ {
"filename" : "cookbook-20@2x.png", "filename" : "cookbook-icon-20@2x.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "2x", "scale" : "2x",
"size" : "20x20" "size" : "20x20"
}, },
{ {
"filename" : "cookbook-29.png", "filename" : "cookbook-icon-29.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "1x", "scale" : "1x",
"size" : "29x29" "size" : "29x29"
}, },
{ {
"filename" : "cookbook-29@2x.png", "filename" : "cookbook-icon-29@2x.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "2x", "scale" : "2x",
"size" : "29x29" "size" : "29x29"
}, },
{ {
"filename" : "cookbook-40.png", "filename" : "cookbook-icon-40.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "1x", "scale" : "1x",
"size" : "40x40" "size" : "40x40"
}, },
{ {
"filename" : "cookbook-40@2x.png", "filename" : "cookbook-icon-40@2x.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "2x", "scale" : "2x",
"size" : "40x40" "size" : "40x40"
}, },
{ {
"filename" : "cookbook-76.png", "filename" : "cookbook-icon-76.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "1x", "scale" : "1x",
"size" : "76x76" "size" : "76x76"
}, },
{ {
"filename" : "cookbook-76@2x.png", "filename" : "cookbook-icon-76@2x.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "2x", "scale" : "2x",
"size" : "76x76" "size" : "76x76"
}, },
{ {
"filename" : "cookbook-83.5@2x.png", "filename" : "cookbook-icon-83.5@2x.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "2x", "scale" : "2x",
"size" : "83.5x83.5" "size" : "83.5x83.5"
}, },
{ {
"filename" : "cookbook-1024.png", "filename" : "cookbook-icon-1024.png",
"idiom" : "ios-marketing", "idiom" : "ios-marketing",
"scale" : "1x", "scale" : "1x",
"size" : "1024x1024" "size" : "1024x1024"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "cookbook-category.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -1,11 +1,11 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "cookbook-icon.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "1x" "scale" : "1x"
}, },
{ {
"filename" : "cookbook.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "2x" "scale" : "2x"
}, },

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "cookbook-recipe.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -18,17 +18,24 @@ extension Formatter {
func formatDate(duration: String) -> String { func formatDate(duration: String) -> String {
var duration = duration var duration = duration
if duration.hasPrefix("PT") { duration.removeFirst(2) } if duration.hasPrefix("PT") { duration.removeFirst(2) }
let hour, minute, second: Double var hour: Int = 0, minute: Int = 0
if let index = duration.firstIndex(of: "H") { if let index = duration.firstIndex(of: "H") {
hour = Double(duration[..<index]) ?? 0 hour = Int(duration[..<index]) ?? 0
duration.removeSubrange(...index) duration.removeSubrange(...index)
} else { hour = 0 } }
if let index = duration.firstIndex(of: "M") { if let index = duration.firstIndex(of: "M") {
minute = Double(duration[..<index]) ?? 0 minute = Int(duration[..<index]) ?? 0
duration.removeSubrange(...index) duration.removeSubrange(...index)
} else { minute = 0 } }
if let index = duration.firstIndex(of: "S") {
second = Double(duration[..<index]) ?? 0 if hour == 0 && minute != 0 {
} else { second = 0 } return "\(minute)min"
return Formatter.positional.string(from: hour * 3600 + minute * 60 + second) ?? "0:00" }
if hour != 0 && minute == 0 {
return "\(hour)h"
}
if hour != 0 && minute != 0 {
return "\(hour)h \(minute)"
}
return "--"
} }

View File

@@ -13,23 +13,19 @@ struct CategoryCardView: View {
var body: some View { var body: some View {
ZStack { ZStack {
Image("CookBook") Image("cookbook-category")
.aspectRatio(1, contentMode: .fit) .resizable()
.scaledToFit()
.overlay( .overlay(
VStack { VStack {
Spacer() Spacer()
Color.clear Text(category.name == "*" ? "Other" : category.name)
.background( .font(.headline)
.ultraThickMaterial .lineLimit(2)
) .foregroundStyle(.white)
.overlay( .padding()
Text(category.name == "*" ? "Other" : category.name)
.font(.headline)
)
.frame(maxHeight: 25)
} }
) )
.clipShape(RoundedRectangle(cornerRadius: 10))
.padding() .padding()
} }
} }

View File

@@ -12,18 +12,17 @@ import SwiftUI
struct RecipeBookView: View { struct RecipeBookView: View {
@State var categoryName: String @State var categoryName: String
@State var searchText: String = ""
@ObservedObject var viewModel: MainViewModel @ObservedObject var viewModel: MainViewModel
var body: some View { var body: some View {
ScrollView(showsIndicators: false) { ScrollView(showsIndicators: false) {
LazyVStack { LazyVStack {
if let recipes = viewModel.recipes[categoryName] { ForEach(recipesFiltered(), id: \.recipe_id) { recipe in
ForEach(recipes, id: \.recipe_id) { recipe in NavigationLink(destination: RecipeDetailView(viewModel: viewModel, recipe: recipe)) {
NavigationLink(destination: RecipeDetailView(viewModel: viewModel, recipe: recipe)) { RecipeCardView(viewModel: viewModel, recipe: recipe)
RecipeCardView(viewModel: viewModel, recipe: recipe)
}
.buttonStyle(.plain)
} }
.buttonStyle(.plain)
} }
} }
} }
@@ -44,6 +43,7 @@ struct RecipeBookView: View {
Image(systemName: "ellipsis.circle") Image(systemName: "ellipsis.circle")
} }
} }
.searchable(text: $searchText, prompt: "Search recipes")
.task { .task {
await viewModel.loadRecipeList(categoryName: categoryName) await viewModel.loadRecipeList(categoryName: categoryName)
} }
@@ -52,6 +52,15 @@ struct RecipeBookView: View {
} }
} }
func recipesFiltered() -> [Recipe] {
guard let recipes = viewModel.recipes[categoryName] else { return [] }
guard searchText != "" else { return recipes }
return recipes.filter { recipe in
recipe.name.lowercased().contains(searchText.lowercased())
}
}
func downloadRecipes() { func downloadRecipes() {
if let recipes = viewModel.recipes[categoryName] { if let recipes = viewModel.recipes[categoryName] {
let dispatchQueue = DispatchQueue(label: "RecipeDownload", qos: .background) let dispatchQueue = DispatchQueue(label: "RecipeDownload", qos: .background)

View File

@@ -0,0 +1,81 @@
//
// KeywordPickerView.swift
// Nextcloud Cookbook iOS Client
//
// Created by Vincent Meilinger on 03.10.23.
//
import Foundation
import SwiftUI
struct KeywordPickerView: View {
@State var title: String
@State var searchSuggestions: [String]
@Binding var selection: [String]
@State var searchText: String = ""
var columns: [GridItem] = [GridItem(.adaptive(minimum: 120), spacing: 0)]
var body: some View {
VStack {
TextField(title, text: $searchText)
.textFieldStyle(.roundedBorder)
.padding()
LazyVGrid(columns: columns, spacing: 5) {
if searchText != "" {
HStack {
if selection.contains(searchText) {
Image(systemName: "checkmark.circle.fill")
}
Text(searchText)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 15)
.foregroundStyle(Color("backgroundHighlight"))
)
.onTapGesture {
if selection.contains(searchText) {
selection.removeAll(where: { s in
s == searchText ? true : false
})
} else {
selection.append(searchText)
searchSuggestions.append(searchText)
}
}
}
ForEach(suggestionsFiltered(), id: \.self) { suggestion in
HStack {
if selection.contains(suggestion) {
Image(systemName: "checkmark.circle.fill")
}
Text(suggestion)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 15)
.foregroundStyle(Color("backgroundHighlight"))
)
.onTapGesture {
if selection.contains(suggestion) {
selection.removeAll(where: { s in
s == suggestion ? true : false
})
} else {
selection.append(suggestion)
}
}
}
}
Spacer()
}
}
func suggestionsFiltered() -> [String] {
guard searchText != "" else { return searchSuggestions }
return searchSuggestions.filter { suggestion in
suggestion.lowercased().contains(searchText.lowercased())
}
}
}

View File

@@ -11,7 +11,7 @@ struct MainView: View {
@ObservedObject var viewModel: MainViewModel @ObservedObject var viewModel: MainViewModel
@ObservedObject var userSettings: UserSettings @ObservedObject var userSettings: UserSettings
@State var showEditView: Bool = false @State private var showEditView: Bool = false
var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 0)] var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 0)]
var body: some View { var body: some View {
@@ -33,6 +33,9 @@ struct MainView: View {
} }
} }
} }
/*.navigationDestination(isPresented: $showEditView) {
RecipeEditView()
}*/
.navigationTitle("Cookbooks") .navigationTitle("Cookbooks")
.toolbar { .toolbar {
Menu { Menu {
@@ -49,6 +52,7 @@ struct MainView: View {
} }
Button { Button {
print("Create recipe")
showEditView = true showEditView = true
} label: { } label: {
HStack { HStack {
@@ -67,6 +71,7 @@ struct MainView: View {
.background( .background(
NavigationLink(destination: RecipeEditView(), isActive: $showEditView) { EmptyView() } NavigationLink(destination: RecipeEditView(), isActive: $showEditView) { EmptyView() }
) )
} }
.tint(.nextcloudBlue) .tint(.nextcloudBlue)
.task { .task {

View File

@@ -29,7 +29,7 @@ struct WelcomeTab: View {
var body: some View { var body: some View {
VStack(alignment: .center) { VStack(alignment: .center) {
Spacer() Spacer()
Image("CookBook") Image("cookbook-icon")
.resizable() .resizable()
.frame(width: 120, height: 120) .frame(width: 120, height: 120)
.clipShape(RoundedRectangle(cornerRadius: 10)) .clipShape(RoundedRectangle(cornerRadius: 10))

View File

@@ -16,7 +16,7 @@ struct RecipeCardView: View {
var body: some View { var body: some View {
HStack { HStack {
Image(uiImage: recipeThumb ?? UIImage(named: "CookBook")!) Image(uiImage: recipeThumb ?? UIImage(named: "cookbook-recipe")!)
.resizable() .resizable()
.aspectRatio(contentMode: .fill) .aspectRatio(contentMode: .fill)
.frame(width: 80, height: 80) .frame(width: 80, height: 80)
@@ -35,7 +35,7 @@ struct RecipeCardView: View {
} }
} }
.background(Color.backgroundHighlight) .background(Color.backgroundHighlight)
.clipShape(RoundedRectangle(cornerRadius: 10)) .clipShape(RoundedRectangle(cornerRadius: 17))
.padding(.horizontal) .padding(.horizontal)
.task { .task {
recipeThumb = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: true) recipeThumb = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: true)

View File

@@ -7,60 +7,124 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import PhotosUI
struct RecipeEditView: View { struct RecipeEditView: View {
@State var recipe: RecipeDetail @State var recipe: RecipeDetail = RecipeDetail()
@State var image: PhotosPickerItem? = nil
@State var times = [Date.zero, Date.zero, Date.zero] @State var times = [Date.zero, Date.zero, Date.zero]
@State var searchText: String = ""
@State var keywords: [String] = []
init(recipe: RecipeDetail? = nil) { init(recipe: RecipeDetail? = nil) {
self.recipe = recipe ?? RecipeDetail() self.recipe = recipe ?? RecipeDetail()
} }
var body: some View { var body: some View {
Form { Form {
TextField("Title", text: $recipe.name) TextField("Title", text: $recipe.name)
TextField("Description", text: $recipe.description)
PhotosPicker(selection: $image, matching: .images, photoLibrary: .shared()) {
Image(systemName: "photo")
.symbolRenderingMode(.multicolor)
}
.buttonStyle(.borderless)
Section() { Section() {
NavigationLink("Keywords") {
KeywordPickerView(title: "Keyword", searchSuggestions: [], selection: $keywords)
}
} header: {
Text("Keywords")
} footer: {
ScrollView(.horizontal) {
HStack {
ForEach(keywords, id: \.self) { keyword in
Text(keyword)
}
}
}
}
Section() {
Picker("Yield/Portions:", selection: $recipe.recipeYield) {
ForEach(0..<99, id: \.self) { i in
Text("\(i)").tag(i)
}
}
.pickerStyle(.menu)
DatePicker("Prep time:", selection: $times[0], displayedComponents: .hourAndMinute) DatePicker("Prep time:", selection: $times[0], displayedComponents: .hourAndMinute)
DatePicker("Cook time:", selection: $times[1], displayedComponents: .hourAndMinute) DatePicker("Cook time:", selection: $times[1], displayedComponents: .hourAndMinute)
DatePicker("Total time:", selection: $times[2], displayedComponents: .hourAndMinute) DatePicker("Total time:", selection: $times[2], displayedComponents: .hourAndMinute)
} }
Section() { EditableListSection(title: "Ingredients", items: $recipe.recipeIngredient)
EditableListSection(title: "Tools", items: $recipe.tool)
List { EditableListSection(title: "Instructions", items: $recipe.recipeInstructions)
ForEach(recipe.recipeInstructions.indices, id: \.self) { ix in }.navigationTitle("New Recipe")
HStack(alignment: .top) { }
Text("\(ix+1).") }
TextEditor(text: $recipe.recipeInstructions[ix])
.multilineTextAlignment(.leading)
}
} struct SearchField: View {
.onMove { indexSet, offset in @State var title: String
recipe.recipeInstructions.move(fromOffsets: indexSet, toOffset: offset) @State var text: String
} @State var searchSuggestions: [String]
.onDelete { indexSet in
recipe.recipeInstructions.remove(atOffsets: indexSet) var body: some View {
TextField(title, text: $text)
.searchSuggestions {
ForEach(searchSuggestions, id: \.self) { suggestion in
Text(suggestion).searchCompletion(suggestion)
}
}
}
}
struct EditableListSection: View {
@State var title: String
@Binding var items: [String]
var body: some View {
Section() {
List {
ForEach(items.indices, id: \.self) { ix in
HStack(alignment: .top) {
Text("\(ix+1).")
.padding(.vertical, 10)
TextEditor(text: $items[ix])
.multilineTextAlignment(.leading)
.textFieldStyle(.plain)
.padding(.vertical, 1)
} }
} }
HStack { .onMove { indexSet, offset in
Spacer() items.move(fromOffsets: indexSet, toOffset: offset)
Text("Add instruction")
Button() {
recipe.recipeInstructions.append("")
} label: {
Image(systemName: "plus.circle.fill")
}
} }
} header: { .onDelete { indexSet in
HStack { items.remove(atOffsets: indexSet)
Text("Ingredients")
Spacer()
EditButton()
} }
} }
HStack {
Spacer()
Text("Add")
Button() {
items.append("")
} label: {
Image(systemName: "plus.circle.fill")
}
}
} header: {
HStack {
Text(title)
Spacer()
EditButton()
}
} }
} }
} }
@@ -72,12 +136,12 @@ struct TimePicker: View {
var body: some View { var body: some View {
HStack { HStack {
Picker("", selection: $hours){ Picker("", selection: $hours) {
ForEach(0..<99, id: \.self) { i in ForEach(0..<99, id: \.self) { i in
Text("\(i) hours").tag(i) Text("\(i) hours").tag(i)
} }
}.pickerStyle(.wheel) }.pickerStyle(.wheel)
Picker("", selection: $minutes){ Picker("", selection: $minutes) {
ForEach(0..<60, id: \.self) { i in ForEach(0..<60, id: \.self) { i in
Text("\(i) min").tag(i) Text("\(i) min").tag(i)
} }
@@ -86,3 +150,4 @@ struct TimePicker: View {
.padding(.horizontal) .padding(.horizontal)
} }
} }

View File

@@ -64,7 +64,7 @@ struct SettingsView: View {
} }
.tint(.red) .tint(.red)
Button("Delete local data.") { Button("Delete local data") {
print("Clear cache.") print("Clear cache.")
alertType = .DELETE_CACHE alertType = .DELETE_CACHE
showAlert = true showAlert = true
@@ -72,7 +72,9 @@ struct SettingsView: View {
.tint(.red) .tint(.red)
} header: { } header: {
Text("Danger Zone") Text("Other")
} footer: {
Text("Deleting local data will not affect the recipe data stored on your server.")
} }
} }
.navigationTitle("Settings") .navigationTitle("Settings")