Keyword suggestions and language support.
This commit is contained in:
@@ -19,16 +19,16 @@ struct CategoryDetailView: View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
LazyVStack {
|
||||
ForEach(recipesFiltered(), id: \.recipe_id) { recipe in
|
||||
NavigationLink() {
|
||||
RecipeDetailView(viewModel: viewModel, recipe: recipe).id(recipe.recipe_id)
|
||||
} label: {
|
||||
NavigationLink(value: recipe) {
|
||||
RecipeCardView(viewModel: viewModel, recipe: recipe)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationDestination(for: Recipe.self) { recipe in
|
||||
RecipeDetailView(viewModel: viewModel, recipe: recipe)//.id(recipe.recipe_id)
|
||||
}
|
||||
.navigationTitle(categoryName == "*" ? "Other" : categoryName)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
|
||||
@@ -31,10 +31,6 @@ struct CategoryPickerView: View {
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.foregroundStyle(Color("backgroundHighlight"))
|
||||
)
|
||||
.onTapGesture {
|
||||
selection = searchText
|
||||
}
|
||||
@@ -47,10 +43,6 @@ struct CategoryPickerView: View {
|
||||
Text(suggestion)
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.foregroundStyle(Color("backgroundHighlight"))
|
||||
)
|
||||
.onTapGesture {
|
||||
selection = suggestion
|
||||
}
|
||||
|
||||
@@ -8,20 +8,14 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct Keyword: Identifiable {
|
||||
let id = UUID()
|
||||
let name: String
|
||||
|
||||
init(_ name: String) {
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct KeywordPickerView: View {
|
||||
@State var title: String
|
||||
@State var searchSuggestions: [Keyword]
|
||||
@State var searchSuggestions: [String]
|
||||
@Binding var selection: [String]
|
||||
@State var searchText: String = ""
|
||||
|
||||
var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 5)]
|
||||
|
||||
var body: some View {
|
||||
@@ -33,32 +27,32 @@ struct KeywordPickerView: View {
|
||||
LazyVGrid(columns: columns, spacing: 5) {
|
||||
if searchText != "" {
|
||||
KeywordItemView(
|
||||
keyword: Keyword(searchText),
|
||||
keyword: searchText,
|
||||
isSelected: selection.contains(searchText)
|
||||
) { keyword in
|
||||
if selection.contains(keyword.name) {
|
||||
if selection.contains(keyword) {
|
||||
selection.removeAll(where: { s in
|
||||
s == keyword.name ? true : false
|
||||
s == keyword ? true : false
|
||||
})
|
||||
searchSuggestions.removeAll(where: { s in
|
||||
s.name == keyword.name ? true : false
|
||||
s == keyword ? true : false
|
||||
})
|
||||
} else {
|
||||
selection.append(keyword.name)
|
||||
selection.append(keyword)
|
||||
}
|
||||
}
|
||||
}
|
||||
ForEach(suggestionsFiltered(), id: \.id) { suggestion in
|
||||
ForEach(suggestionsFiltered(), id: \.self) { suggestion in
|
||||
KeywordItemView(
|
||||
keyword: suggestion,
|
||||
isSelected: selection.contains(suggestion.name)
|
||||
isSelected: selection.contains(suggestion)
|
||||
) { keyword in
|
||||
if selection.contains(keyword.name) {
|
||||
if selection.contains(keyword) {
|
||||
selection.removeAll(where: { s in
|
||||
s == keyword.name ? true : false
|
||||
s == keyword ? true : false
|
||||
})
|
||||
} else {
|
||||
selection.append(keyword.name)
|
||||
selection.append(keyword)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,15 +67,15 @@ struct KeywordPickerView: View {
|
||||
LazyVGrid(columns: columns, spacing: 5) {
|
||||
ForEach(selection, id: \.self) { suggestion in
|
||||
KeywordItemView(
|
||||
keyword: Keyword(suggestion),
|
||||
keyword: suggestion,
|
||||
isSelected: true
|
||||
) { keyword in
|
||||
if selection.contains(keyword.name) {
|
||||
if selection.contains(keyword) {
|
||||
selection.removeAll(where: { s in
|
||||
s == keyword.name ? true : false
|
||||
s == keyword ? true : false
|
||||
})
|
||||
} else {
|
||||
selection.append(keyword.name)
|
||||
selection.append(keyword)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,12 +84,13 @@ struct KeywordPickerView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle(title)
|
||||
|
||||
}
|
||||
|
||||
func suggestionsFiltered() -> [Keyword] {
|
||||
func suggestionsFiltered() -> [String] {
|
||||
guard searchText != "" else { return searchSuggestions }
|
||||
return searchSuggestions.filter { suggestion in
|
||||
suggestion.name.lowercased().contains(searchText.lowercased())
|
||||
suggestion.lowercased().contains(searchText.lowercased())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,17 +98,18 @@ struct KeywordPickerView: View {
|
||||
|
||||
|
||||
struct KeywordItemView: View {
|
||||
var keyword: Keyword
|
||||
var keyword: String
|
||||
var isSelected: Bool
|
||||
var tapped: (Keyword) -> ()
|
||||
var tapped: (String) -> ()
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
}
|
||||
Text(keyword.name)
|
||||
Text(keyword)
|
||||
.lineLimit(2)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
|
||||
@@ -14,6 +14,8 @@ struct MainView: View {
|
||||
|
||||
@State private var selectedCategory: Category? = nil
|
||||
@State private var showEditView: Bool = false
|
||||
@State private var showSettingsView: Bool = false
|
||||
|
||||
var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 0)]
|
||||
|
||||
var body: some View {
|
||||
@@ -31,6 +33,9 @@ struct MainView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Cookbooks")
|
||||
.navigationDestination(isPresented: $showSettingsView) {
|
||||
SettingsView(userSettings: userSettings, viewModel: viewModel)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
Menu {
|
||||
@@ -47,26 +52,27 @@ struct MainView: View {
|
||||
}
|
||||
|
||||
Button {
|
||||
print("Create recipe")
|
||||
showEditView = true
|
||||
self.showSettingsView = true
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Create new recipe")
|
||||
Image(systemName: "plus.circle")
|
||||
}
|
||||
Text("Settings")
|
||||
Image(systemName: "gearshape")
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
NavigationLink( destination: SettingsView(userSettings: userSettings, viewModel: viewModel)) {
|
||||
Image(systemName: "gearshape")
|
||||
Button {
|
||||
print("Create recipe")
|
||||
showEditView = true
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} detail: {
|
||||
NavigationStack {
|
||||
if let category = selectedCategory {
|
||||
@@ -77,7 +83,6 @@ struct MainView: View {
|
||||
.id(category.id) // Workaround: This is needed to update the detail view when the selection changes
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.tint(.nextcloudBlue)
|
||||
.sheet(isPresented: $showEditView) {
|
||||
|
||||
@@ -33,13 +33,13 @@ struct WelcomeTab: View {
|
||||
.resizable()
|
||||
.frame(width: 120, height: 120)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
Text("Tank you for downloading the")
|
||||
Text("Tank you for downloading")
|
||||
.font(.headline)
|
||||
Text("Cookbook Client")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
Spacer()
|
||||
Text("This application is an open source effort and still in development. If you encounter any problems, please report them on our GitHub page.")
|
||||
Text("This application is an open source effort. If you're interested in suggesting or contributing new features, or you encounter any problems, please use the support link or visit the GitHub repository in the app settings.")
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ struct RecipeDurationSection: View {
|
||||
HStack(alignment: .center) {
|
||||
if let prepTime = recipeDetail.prepTime {
|
||||
VStack {
|
||||
SecondaryLabel(text: "Prep time")
|
||||
SecondaryLabel(text: String(localized: "Prep time"))
|
||||
Text(DateFormatter.formatDate(duration: prepTime))
|
||||
.lineLimit(1)
|
||||
}.padding()
|
||||
@@ -115,7 +115,7 @@ struct RecipeDurationSection: View {
|
||||
|
||||
if let cookTime = recipeDetail.cookTime {
|
||||
VStack {
|
||||
SecondaryLabel(text: "Cook time")
|
||||
SecondaryLabel(text: String(localized: "Cook time"))
|
||||
Text(DateFormatter.formatDate(duration: cookTime))
|
||||
.lineLimit(1)
|
||||
}.padding()
|
||||
@@ -123,7 +123,7 @@ struct RecipeDurationSection: View {
|
||||
|
||||
if let totalTime = recipeDetail.totalTime {
|
||||
VStack {
|
||||
SecondaryLabel(text: "Total time")
|
||||
SecondaryLabel(text: String(localized: "Total time"))
|
||||
Text(DateFormatter.formatDate(duration: totalTime))
|
||||
.lineLimit(1)
|
||||
}.padding()
|
||||
@@ -140,11 +140,11 @@ struct RecipeIngredientSection: View {
|
||||
Divider()
|
||||
HStack {
|
||||
if recipeDetail.recipeYield == 0 {
|
||||
SecondaryLabel(text: "Ingredients")
|
||||
SecondaryLabel(text: String(localized: "Ingredients"))
|
||||
} else if recipeDetail.recipeYield == 1 {
|
||||
SecondaryLabel(text: "Ingredients per serving")
|
||||
SecondaryLabel(text: String(localized: "Ingredients per serving"))
|
||||
} else {
|
||||
SecondaryLabel(text: "Ingredients for \(recipeDetail.recipeYield) servings")
|
||||
SecondaryLabel(text: String(localized: "Ingredients for \(recipeDetail.recipeYield) servings"))
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
@@ -166,7 +166,7 @@ struct RecipeToolSection: View {
|
||||
VStack(alignment: .leading) {
|
||||
Divider()
|
||||
HStack {
|
||||
SecondaryLabel(text: "Tools")
|
||||
SecondaryLabel(text: String(localized: "Tools"))
|
||||
Spacer()
|
||||
}
|
||||
ForEach(recipeDetail.tool, id: \.self) { tool in
|
||||
@@ -187,7 +187,7 @@ struct RecipeInstructionSection: View {
|
||||
VStack(alignment: .leading) {
|
||||
Divider()
|
||||
HStack {
|
||||
SecondaryLabel(text: "Instructions")
|
||||
SecondaryLabel(text: String(localized: "Instructions"))
|
||||
Spacer()
|
||||
}
|
||||
ForEach(0..<recipeDetail.recipeInstructions.count) { ix in
|
||||
|
||||
@@ -65,120 +65,122 @@ struct RecipeEditView: View {
|
||||
@State private var times = [Date.zero, Date.zero, Date.zero]
|
||||
@State private var searchText: String = ""
|
||||
@State private var keywords: [String] = []
|
||||
@State private var keywordSuggestions: [String] = []
|
||||
|
||||
@State private var alertMessage: ErrorMessages = .GENERIC
|
||||
@State private var presentAlert: Bool = false
|
||||
@State private var waitingForUpload: Bool = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Button() {
|
||||
isPresented = false
|
||||
} label: {
|
||||
Text("Cancel")
|
||||
.bold()
|
||||
}
|
||||
if !uploadNew {
|
||||
Menu {
|
||||
Button {
|
||||
print("Delete recipe.")
|
||||
alertMessage = .CONFIRM_DELETE
|
||||
presentAlert = true
|
||||
NavigationStack {
|
||||
VStack {
|
||||
HStack {
|
||||
Button() {
|
||||
isPresented = false
|
||||
} label: {
|
||||
Text("Cancel")
|
||||
.bold()
|
||||
}
|
||||
if !uploadNew {
|
||||
Menu {
|
||||
Button {
|
||||
print("Delete recipe.")
|
||||
alertMessage = .CONFIRM_DELETE
|
||||
presentAlert = true
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
.foregroundStyle(.red)
|
||||
Text("Delete recipe")
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
.foregroundStyle(.red)
|
||||
Text("Delete recipe")
|
||||
.foregroundStyle(.red)
|
||||
Image(systemName: "ellipsis.circle")
|
||||
.font(.title3)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Button() {
|
||||
if uploadNew {
|
||||
uploadNewRecipe()
|
||||
} else {
|
||||
uploadEditedRecipe()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
.font(.title3)
|
||||
.padding()
|
||||
Text("Upload")
|
||||
.bold()
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Button() {
|
||||
if uploadNew {
|
||||
uploadNewRecipe()
|
||||
} else {
|
||||
uploadEditedRecipe()
|
||||
}
|
||||
} label: {
|
||||
Text("Upload")
|
||||
}.padding()
|
||||
HStack {
|
||||
Text(recipe.name == "" ? String(localized: "New recipe") : recipe.name)
|
||||
.font(.title)
|
||||
.bold()
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
}.padding()
|
||||
HStack {
|
||||
Text(recipe.name == "" ? "New recipe" : recipe.name)
|
||||
.font(.title)
|
||||
.bold()
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
Form {
|
||||
TextField("Title", text: $recipe.name)
|
||||
Section {
|
||||
TextEditor(text: $recipe.description)
|
||||
} header: {
|
||||
Text("Description")
|
||||
}
|
||||
/*
|
||||
PhotosPicker(selection: $image, matching: .images, photoLibrary: .shared()) {
|
||||
Image(systemName: "photo")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
*/
|
||||
Section() {
|
||||
NavigationLink(recipe.recipeCategory == "" ? "Category" : "Category: \(recipe.recipeCategory)") {
|
||||
CategoryPickerView(title: "Category", searchSuggestions: [], selection: $recipe.recipeCategory)
|
||||
Form {
|
||||
TextField("Title", text: $recipe.name)
|
||||
Section {
|
||||
TextEditor(text: $recipe.description)
|
||||
} header: {
|
||||
Text("Description")
|
||||
}
|
||||
NavigationLink("Keywords") {
|
||||
KeywordPickerView(
|
||||
title: "Keywords",
|
||||
searchSuggestions: [
|
||||
Keyword("Hauptspeisen"),
|
||||
Keyword("Lecker"),
|
||||
Keyword("Trinken"),
|
||||
Keyword("Essen"),
|
||||
Keyword("Nachspeisen"),
|
||||
Keyword("Futter"),
|
||||
Keyword("Apfel"),
|
||||
Keyword("test")
|
||||
],
|
||||
selection: $keywords
|
||||
)
|
||||
}
|
||||
} header: {
|
||||
Text("Discoverability")
|
||||
} footer: {
|
||||
ScrollView(.horizontal) {
|
||||
HStack {
|
||||
ForEach(keywords, id: \.self) { keyword in
|
||||
Text(keyword)
|
||||
/*
|
||||
PhotosPicker(selection: $image, matching: .images, photoLibrary: .shared()) {
|
||||
Image(systemName: "photo")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
*/
|
||||
Section() {
|
||||
NavigationLink(recipe.recipeCategory == "" ? "Category" : "Category: \(recipe.recipeCategory)") {
|
||||
CategoryPickerView(
|
||||
title: "Category",
|
||||
searchSuggestions: viewModel.categories.map({ category in
|
||||
category.name == "*" ? "Other" : category.name
|
||||
}),
|
||||
selection: $recipe.recipeCategory)
|
||||
}
|
||||
NavigationLink("Keywords") {
|
||||
KeywordPickerView(
|
||||
title: "Keywords",
|
||||
searchSuggestions: keywordSuggestions,
|
||||
selection: $keywords
|
||||
)
|
||||
}
|
||||
} header: {
|
||||
Text("Discoverability")
|
||||
} footer: {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
.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)
|
||||
|
||||
EditableListSection(title: String(localized: "Ingredients"), items: $recipe.recipeIngredient)
|
||||
EditableListSection(title: String(localized: "Tools"), items: $recipe.tool)
|
||||
EditableListSection(title: String(localized: "Instructions"), items: $recipe.recipeInstructions)
|
||||
}
|
||||
|
||||
EditableListSection(title: "Ingredients", items: $recipe.recipeIngredient)
|
||||
EditableListSection(title: "Tools", items: $recipe.tool)
|
||||
EditableListSection(title: "Instructions", items: $recipe.recipeInstructions)
|
||||
}
|
||||
}
|
||||
.task {
|
||||
self.keywordSuggestions = await viewModel.getKeywords()
|
||||
}
|
||||
.onAppear {
|
||||
if uploadNew { return }
|
||||
if let prepTime = recipe.prepTime {
|
||||
@@ -301,6 +303,9 @@ struct RecipeEditView: View {
|
||||
]
|
||||
)
|
||||
sendRequest(request)
|
||||
if let recipeIdInt = Int(recipe.id) {
|
||||
viewModel.deleteRecipe(withId: recipeIdInt, categoryName: recipe.recipeCategory)
|
||||
}
|
||||
dismissEditView()
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,22 @@ fileprivate enum SettingsAlert {
|
||||
}
|
||||
}
|
||||
|
||||
enum SupportedLanguage: String, Codable {
|
||||
case EN = "en",
|
||||
DE = "de"
|
||||
|
||||
func descriptor() -> String {
|
||||
switch self {
|
||||
case .EN:
|
||||
return "English"
|
||||
case .DE:
|
||||
return "Deutsch"
|
||||
}
|
||||
}
|
||||
|
||||
static let allValues = [EN, DE]
|
||||
}
|
||||
|
||||
struct SettingsView: View {
|
||||
@ObservedObject var userSettings: UserSettings
|
||||
@ObservedObject var viewModel: MainViewModel
|
||||
@@ -40,21 +56,21 @@ struct SettingsView: View {
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
Picker("Select a cookbook", selection: $userSettings.defaultCategory) {
|
||||
Text("")
|
||||
Picker("Select a default cookbook", selection: $userSettings.defaultCategory) {
|
||||
Text("None").tag("None")
|
||||
ForEach(viewModel.categories, id: \.name) { category in
|
||||
Text(category.name == "*" ? "Other" : category.name)
|
||||
Text(category.name == "*" ? "Other" : category.name).tag(category)
|
||||
}
|
||||
}
|
||||
Button {
|
||||
userSettings.defaultCategory = ""
|
||||
} label: {
|
||||
Text("Clear default category")
|
||||
Picker("Language", selection: $userSettings.language) {
|
||||
ForEach(SupportedLanguage.allValues, id: \.self) { lang in
|
||||
Text(lang.descriptor()).tag(lang.rawValue)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Default cookbook")
|
||||
Text("General")
|
||||
} footer: {
|
||||
Text("The selected cookbook will be opened on app launch by default.")
|
||||
Text("The selected cookbook will open on app launch by default.")
|
||||
}
|
||||
Section() {
|
||||
Link("Visit the GitHub page", destination: URL(string: "https://github.com/VincentMeilinger/Nextcloud-Cookbook-iOS")!)
|
||||
@@ -105,6 +121,7 @@ struct SettingsView: View {
|
||||
} message: {
|
||||
Text(alertType.getMessage())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func logOut() {
|
||||
@@ -116,7 +133,7 @@ struct SettingsView: View {
|
||||
}
|
||||
|
||||
func deleteCache() {
|
||||
//viewModel.deleteAllData()
|
||||
viewModel.deleteAllData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user