Recipes are now searchable
@@ -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;
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
21
Nextcloud Cookbook iOS Client/Assets.xcassets/cookbook-category.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Nextcloud Cookbook iOS Client/Assets.xcassets/cookbook-category.imageset/cookbook-category.png
vendored
Normal file
|
After Width: | Height: | Size: 86 KiB |
@@ -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"
|
||||||
},
|
},
|
||||||
BIN
Nextcloud Cookbook iOS Client/Assets.xcassets/cookbook-icon.imageset/cookbook-icon.png
vendored
Normal file
|
After Width: | Height: | Size: 58 KiB |
21
Nextcloud Cookbook iOS Client/Assets.xcassets/cookbook-recipe.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Nextcloud Cookbook iOS Client/Assets.xcassets/cookbook-recipe.imageset/cookbook-recipe.png
vendored
Normal file
|
After Width: | Height: | Size: 51 KiB |
@@ -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 "--"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
81
Nextcloud Cookbook iOS Client/Views/KeywordPickerView.swift
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
EditableListSection(title: "Instructions", items: $recipe.recipeInstructions)
|
||||||
|
}.navigationTitle("New Recipe")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
List {
|
|
||||||
ForEach(recipe.recipeInstructions.indices, id: \.self) { ix in
|
|
||||||
HStack(alignment: .top) {
|
struct SearchField: View {
|
||||||
Text("\(ix+1).")
|
@State var title: String
|
||||||
TextEditor(text: $recipe.recipeInstructions[ix])
|
@State var text: String
|
||||||
.multilineTextAlignment(.leading)
|
@State var searchSuggestions: [String]
|
||||||
}
|
|
||||||
}
|
var body: some View {
|
||||||
.onMove { indexSet, offset in
|
TextField(title, text: $text)
|
||||||
recipe.recipeInstructions.move(fromOffsets: indexSet, toOffset: offset)
|
.searchSuggestions {
|
||||||
}
|
ForEach(searchSuggestions, id: \.self) { suggestion in
|
||||||
.onDelete { indexSet in
|
Text(suggestion).searchCompletion(suggestion)
|
||||||
recipe.recipeInstructions.remove(atOffsets: indexSet)
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||