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

View File

@@ -1,109 +1,109 @@
{
"images" : [
{
"filename" : "cookbook-20@2x.png",
"filename" : "cookbook-icon-20@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "cookbook-20@3x.png",
"filename" : "cookbook-icon-20@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "cookbook-29@2x.png",
"filename" : "cookbook-icon-29@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "cookbook-29@3x.png",
"filename" : "cookbook-icon-29@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "cookbook-40@2x.png",
"filename" : "cookbook-icon-40@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "cookbook-40@3x.png",
"filename" : "cookbook-icon-40@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "cookbook-60@2x.png",
"filename" : "cookbook-icon-60@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "cookbook-60@3x.png",
"filename" : "cookbook-icon-60@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "cookbook-20.png",
"filename" : "cookbook-icon-20.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "cookbook-20@2x.png",
"filename" : "cookbook-icon-20@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "cookbook-29.png",
"filename" : "cookbook-icon-29.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "cookbook-29@2x.png",
"filename" : "cookbook-icon-29@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "cookbook-40.png",
"filename" : "cookbook-icon-40.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "cookbook-40@2x.png",
"filename" : "cookbook-icon-40@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "cookbook-76.png",
"filename" : "cookbook-icon-76.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "cookbook-76@2x.png",
"filename" : "cookbook-icon-76@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "cookbook-83.5@2x.png",
"filename" : "cookbook-icon-83.5@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "cookbook-1024.png",
"filename" : "cookbook-icon-1024.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"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" : [
{
"filename" : "cookbook-icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "cookbook.png",
"idiom" : "universal",
"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 {
var duration = duration
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") {
hour = Double(duration[..<index]) ?? 0
hour = Int(duration[..<index]) ?? 0
duration.removeSubrange(...index)
} else { hour = 0 }
}
if let index = duration.firstIndex(of: "M") {
minute = Double(duration[..<index]) ?? 0
minute = Int(duration[..<index]) ?? 0
duration.removeSubrange(...index)
} else { minute = 0 }
if let index = duration.firstIndex(of: "S") {
second = Double(duration[..<index]) ?? 0
} else { second = 0 }
return Formatter.positional.string(from: hour * 3600 + minute * 60 + second) ?? "0:00"
}
if hour == 0 && minute != 0 {
return "\(minute)min"
}
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 {
ZStack {
Image("CookBook")
.aspectRatio(1, contentMode: .fit)
Image("cookbook-category")
.resizable()
.scaledToFit()
.overlay(
VStack {
Spacer()
Color.clear
.background(
.ultraThickMaterial
)
.overlay(
Text(category.name == "*" ? "Other" : category.name)
.font(.headline)
)
.frame(maxHeight: 25)
Text(category.name == "*" ? "Other" : category.name)
.font(.headline)
.lineLimit(2)
.foregroundStyle(.white)
.padding()
}
)
.clipShape(RoundedRectangle(cornerRadius: 10))
.padding()
}
}

View File

@@ -12,18 +12,17 @@ import SwiftUI
struct RecipeBookView: View {
@State var categoryName: String
@State var searchText: String = ""
@ObservedObject var viewModel: MainViewModel
var body: some View {
ScrollView(showsIndicators: false) {
LazyVStack {
if let recipes = viewModel.recipes[categoryName] {
ForEach(recipes, id: \.recipe_id) { recipe in
NavigationLink(destination: RecipeDetailView(viewModel: viewModel, recipe: recipe)) {
RecipeCardView(viewModel: viewModel, recipe: recipe)
}
.buttonStyle(.plain)
ForEach(recipesFiltered(), id: \.recipe_id) { recipe in
NavigationLink(destination: RecipeDetailView(viewModel: viewModel, recipe: recipe)) {
RecipeCardView(viewModel: viewModel, recipe: recipe)
}
.buttonStyle(.plain)
}
}
}
@@ -44,6 +43,7 @@ struct RecipeBookView: View {
Image(systemName: "ellipsis.circle")
}
}
.searchable(text: $searchText, prompt: "Search recipes")
.task {
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() {
if let recipes = viewModel.recipes[categoryName] {
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 userSettings: UserSettings
@State var showEditView: Bool = false
@State private var showEditView: Bool = false
var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 0)]
var body: some View {
@@ -33,6 +33,9 @@ struct MainView: View {
}
}
}
/*.navigationDestination(isPresented: $showEditView) {
RecipeEditView()
}*/
.navigationTitle("Cookbooks")
.toolbar {
Menu {
@@ -49,6 +52,7 @@ struct MainView: View {
}
Button {
print("Create recipe")
showEditView = true
} label: {
HStack {
@@ -67,6 +71,7 @@ struct MainView: View {
.background(
NavigationLink(destination: RecipeEditView(), isActive: $showEditView) { EmptyView() }
)
}
.tint(.nextcloudBlue)
.task {

View File

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

View File

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

View File

@@ -7,60 +7,124 @@
import Foundation
import SwiftUI
import PhotosUI
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 searchText: String = ""
@State var keywords: [String] = []
init(recipe: RecipeDetail? = nil) {
self.recipe = recipe ?? RecipeDetail()
}
var body: some View {
Form {
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() {
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("Cook time:", selection: $times[1], 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) {
Text("\(ix+1).")
TextEditor(text: $recipe.recipeInstructions[ix])
.multilineTextAlignment(.leading)
}
}
.onMove { indexSet, offset in
recipe.recipeInstructions.move(fromOffsets: indexSet, toOffset: offset)
}
.onDelete { indexSet in
recipe.recipeInstructions.remove(atOffsets: indexSet)
struct SearchField: View {
@State var title: String
@State var text: String
@State var searchSuggestions: [String]
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 {
Spacer()
Text("Add instruction")
Button() {
recipe.recipeInstructions.append("")
} label: {
Image(systemName: "plus.circle.fill")
}
.onMove { indexSet, offset in
items.move(fromOffsets: indexSet, toOffset: offset)
}
} header: {
HStack {
Text("Ingredients")
Spacer()
EditButton()
.onDelete { indexSet in
items.remove(atOffsets: indexSet)
}
}
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 {
HStack {
Picker("", selection: $hours){
Picker("", selection: $hours) {
ForEach(0..<99, id: \.self) { i in
Text("\(i) hours").tag(i)
}
}.pickerStyle(.wheel)
Picker("", selection: $minutes){
Picker("", selection: $minutes) {
ForEach(0..<60, id: \.self) { i in
Text("\(i) min").tag(i)
}
@@ -86,3 +150,4 @@ struct TimePicker: View {
.padding(.horizontal)
}
}

View File

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