Keyword suggestions and language support.

This commit is contained in:
Vicnet
2023-10-10 13:37:46 +02:00
parent 6780868916
commit c0a63d7560
16 changed files with 986 additions and 169 deletions

View File

@@ -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) {

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

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