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

@@ -32,6 +32,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 */; };
A7AEAE642AD5521400135378 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = A7AEAE632AD5521400135378 /* Localizable.xcstrings */; };
A7F3F8E82ACBFC760076C227 /* KeywordPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F3F8E72ACBFC760076C227 /* KeywordPickerView.swift */; };
A7F3F8EA2ACC221C0076C227 /* CategoryPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F3F8E92ACC221C0076C227 /* CategoryPickerView.swift */; };
/* End PBXBuildFile 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>"; };
A7AEAE632AD5521400135378 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
A7F3F8E72ACBFC760076C227 /* KeywordPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordPickerView.swift; sourceTree = "<group>"; };
A7F3F8E92ACC221C0076C227 /* CategoryPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -142,6 +144,7 @@
A70171B22AB211F000064C43 /* Network */,
A703226B2ABAF60D00D7C4ED /* Extensions */,
A70171852AA8E71F00064C43 /* Assets.xcassets */,
A7AEAE632AD5521400135378 /* Localizable.xcstrings */,
A70171872AA8E71F00064C43 /* Nextcloud_Cookbook_iOS_Client.entitlements */,
A70171882AA8E71F00064C43 /* Preview Content */,
);
@@ -315,6 +318,7 @@
knownRegions = (
en,
Base,
de,
);
mainGroup = A70171752AA8E71900064C43;
productRefGroup = A701717F2AA8E71900064C43 /* Products */;
@@ -335,6 +339,7 @@
files = (
A701718A2AA8E71F00064C43 /* Preview Assets.xcassets in Resources */,
A70171862AA8E71F00064C43 /* Assets.xcassets in Resources */,
A7AEAE642AD5521400135378 /* Localizable.xcstrings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -556,7 +561,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.2;
MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
@@ -597,7 +602,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.2;
MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;

View File

@@ -115,6 +115,11 @@ struct RecipeImage {
var full: UIImage?
}
struct RecipeKeyword: Codable {
let name: String
let recipe_count: Int
}

View File

@@ -56,6 +56,13 @@ class DataStore {
}
}
func delete(path: String) {
Task {
let fileURL = try Self.fileURL(appending: path)
try fileManager.removeItem(at: fileURL)
}
}
func recipeDetailExists(recipeId: Int) -> Bool {
let filePath = "recipe\(recipeId).data"
guard let folderPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first?.path() else { return false }

View File

@@ -40,11 +40,18 @@ class UserSettings: ObservableObject {
}
}
@Published var language: String {
didSet {
UserDefaults.standard.set(language, forKey: "language")
}
}
init() {
self.username = UserDefaults.standard.object(forKey: "username") as? String ?? ""
self.token = UserDefaults.standard.object(forKey: "token") as? String ?? ""
self.serverAddress = UserDefaults.standard.object(forKey: "serverAddress") as? String ?? ""
self.onboarding = UserDefaults.standard.object(forKey: "onboarding") as? Bool ?? true
self.defaultCategory = UserDefaults.standard.object(forKey: "defaultCategory") as? String ?? ""
self.language = UserDefaults.standard.object(forKey: "language") as? String ?? SupportedLanguage.EN.rawValue
}
}

View File

@@ -0,0 +1,746 @@
{
"sourceLanguage" : "en",
"strings" : {
"" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : ""
}
}
}
},
"%@" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@"
}
}
}
},
"%lld" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld"
}
}
}
},
"%lld hours" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld Std."
}
}
}
},
"%lld min" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld Min."
}
}
}
},
"%lld." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld."
}
}
}
},
"•" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "•"
}
}
}
},
"A recipe with that name already exists." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ein Rezept mit diesem Namen existiert bereits."
}
}
}
},
"About" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Über uns"
}
}
}
},
"Add" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hinzufügen"
}
}
}
},
"An unknown error occured." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ein unbekannter Fehler ist aufgetreten."
}
}
}
},
"App Token Login" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "App Token Login"
}
}
}
},
"Cancel" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Abbrechen"
}
}
}
},
"Category" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kategorie"
}
}
}
},
"Category: %@" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kategorie: %@"
}
}
}
},
"Cook time" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kochzeit"
}
}
}
},
"Cook time:" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kochdauer:"
}
}
}
},
"Cookbook Client" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Cookbook Client"
}
}
}
},
"Cookbooks" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kochbücher"
}
}
}
},
"Delete" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Löschen"
}
}
}
},
"Delete local data" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Lokale Daten löschen"
}
}
}
},
"Delete recipe" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Rezept Löschen"
}
}
}
},
"Delete recipe?" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Löschen bestätigen."
}
}
}
},
"Deleting local data will not affect the recipe data stored on your server." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Das Löschen lokaler Daten hat keine Auswirkungen auf die Rezeptdaten, die auf Ihrem Server gespeichert sind."
}
}
}
},
"Description" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Beschreibung"
}
}
}
},
"Discoverability" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kategorisierung"
}
}
}
},
"Download all recipes" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Alle Rezepte herunterladen"
}
}
}
},
"Download recipes" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Rezepte herunterladen"
}
}
}
},
"Duplicate recipe." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Rezept bereits vorhanden."
}
}
}
},
"Edit" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bearbeiten"
}
}
}
},
"Entering the server address will open a web browser. Please follow the login instructions provided there. If the browser does not open, click the link 'Open in browser'\nAfter a successfull login, return to this application and press 'Validate'." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Das Eingeben der Serveradresse wird einen Webbrowser öffnen. Bitte folgen Sie dort den bereitgestellten Anmeldeanweisungen. Falls der Browser nicht geöffnet wird, klicken Sie auf den Link 'Im Browser öffnen'.\nNach erfolgreicher Anmeldung kehren Sie zu dieser Anwendung zurück und drücken Sie 'Überprüfen'."
}
}
}
},
"Error." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Fehler."
}
}
}
},
"General" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Allgemein"
}
}
}
},
"Get support" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kontakt-Seite öffnen"
}
}
}
},
"If you are interested in contributing to this project or simply wish to review its source code, we encourage you to visit the GitHub repository for this application." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Wenn Sie Interesse daran haben, zu diesem Projekt beizutragen oder einfach den Quellcode überprüfen möchten, ermutigen wir Sie, das GitHub-Repository für diese Anwendung zu besuchen."
}
}
}
},
"If you have any inquiries, feedback, or require assistance, please refer to the support page for contact information." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Wenn Sie Anfragen oder Rückmeldungen haben, oder Unterstützung benötigen, finden Sie unter diesem Link die Kontaktinformationen."
}
}
}
},
"Ingredients" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Zutaten"
}
}
}
},
"Ingredients for %lld servings" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Zutaten für %lld Portionen"
}
}
}
},
"Ingredients per serving" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Zutaten pro Portion"
}
}
}
},
"Instructions" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Anleitung"
}
}
}
},
"Keywords" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Schlagwörter"
}
}
}
},
"Language" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sprache"
}
}
}
},
"Log out" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Abmelden"
}
}
}
},
"Login Method" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Login-Methode"
}
}
}
},
"Missing recipe name." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Fehlender Rezeptname."
}
}
}
},
"Network error." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Netzwerkfehler"
}
}
}
},
"New recipe" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Neues Rezept"
}
}
}
},
"Nextcloud Login" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nextcloud Login"
}
}
}
},
"None" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Keines"
}
}
}
},
"Ok" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ok"
}
}
}
},
"Open in browser" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Im Browser öffnen"
}
}
}
},
"Other" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Andere"
}
}
}
},
"Please enter a recipe name." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bitte tragen Sie einen Rezeptnamen ein."
}
}
}
},
"Prep time" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Vorbereitungszeit"
}
}
}
},
"Prep time:" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Vorbereitungszeit"
}
}
}
},
"Search recipes" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Rezepte durchsuchen"
}
}
}
},
"Select a default cookbook" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Wählen Sie ein Standard-Kochbuch"
}
}
}
},
"Selected keywords:" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ausgewählte Schlagwörter:"
}
}
}
},
"Settings" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Einstellungen"
}
}
}
},
"Submit" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Eingeben"
}
}
}
},
"Support" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kontakt"
}
}
}
},
"Tank you for downloading" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Vielen Dank für's herunterladen!"
}
}
}
},
"The selected cookbook will open on app launch by default." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Das ausgewählte Kochbuch wird standardmäßig beim Start der App geöffnet."
}
}
}
},
"This action is not reversible!" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Diese Aktion lässt sich nicht Rückgängig machen!"
}
}
}
},
"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." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Diese Anwendung ist ein Open-Source-Projekt. Wenn Sie daran interessiert sind, neue Funktionen vorzuschlagen oder beizutragen, oder wenn Sie auf Probleme stoßen, nutzen Sie bitte den Kontakt-Link oder besuchen Sie das GitHub-Repository in den App-Einstellungen."
}
}
}
},
"Title" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Name"
}
}
}
},
"Tools" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Küchenutensilien"
}
}
}
},
"Total time" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Gesamtdauer"
}
}
}
},
"Total time:" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Gesamtdauer:"
}
}
}
},
"Unable to upload your recipe. Please check your internet connection." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Es ist nicht möglich, Ihr Rezept hochzuladen. Bitte überprüfen Sie Ihre Internetverbindung."
}
}
}
},
"Upload" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Speichern"
}
}
}
},
"Validate" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Überprüfen"
}
}
}
},
"Visit the GitHub page" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "GitHub öffnen"
}
}
}
},
"Yield/Portions:" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Portionen:"
}
}
}
}
},
"version" : "1.0"
}

View File

@@ -19,7 +19,8 @@ enum RequestPath {
RECIPE_LIST(categoryName: String),
RECIPE_DETAIL(recipeId: Int),
NEW_RECIPE,
IMAGE(recipeId: Int, thumb: Bool)
IMAGE(recipeId: Int, thumb: Bool),
KEYWORDS
case LOGINV2REQ,
CUSTOM(path: String),
@@ -32,6 +33,7 @@ enum RequestPath {
case .RECIPE_DETAIL(recipeId: let recipeId): return "recipes/\(recipeId)"
case .IMAGE(recipeId: let recipeId, thumb: let thumb): return "recipes/\(recipeId)/image?size=\(thumb ? "thumb" : "full")"
case .NEW_RECIPE: return "recipes"
case .KEYWORDS: return "keywords"
case .LOGINV2REQ: return "/index.php/login/v2"
case .CUSTOM(path: let path): return path

View File

@@ -11,6 +11,8 @@ import SwiftUI
struct Nextcloud_Cookbook_iOS_ClientApp: App {
@StateObject var userSettings = UserSettings()
@StateObject var mainViewModel = MainViewModel()
@State(initialValue: "en") var language: String
var body: some Scene {
WindowGroup {
ZStack {
@@ -22,7 +24,14 @@ struct Nextcloud_Cookbook_iOS_ClientApp: App {
mainViewModel.apiController = APIController(userSettings: userSettings)
}
}
}.transition(.slide)
}
}
.transition(.slide)
.onAppear {
language = userSettings.language
print(userSettings.language)
}
.environment(\.locale, .init(identifier: language))
}
}
}

View File

@@ -175,6 +175,17 @@ import SwiftUI
return nil
}
func getKeywords() async -> [String] {
if let keywords: [RecipeKeyword] = await self.loadObject(
localPath: "keywords.data",
networkPath: .KEYWORDS,
needsUpdate: true
) {
return keywords.map { $0.name }
}
return []
}
func deleteAllData() {
if dataStore.clearAll() {
self.categories = []
@@ -183,6 +194,16 @@ import SwiftUI
self.recipeDetails = [:]
}
}
func deleteRecipe(withId id: Int, categoryName: String) {
let path = "recipe\(id).data"
dataStore.delete(path: path)
guard recipes[categoryName] != nil else { return }
recipes[categoryName]!.removeAll(where: { recipe in
recipe.recipe_id == id ? true : false
})
recipeDetails.removeValue(forKey: id)
}
}

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,12 +65,14 @@ 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 {
NavigationStack {
VStack {
HStack {
Button() {
@@ -110,7 +112,7 @@ struct RecipeEditView: View {
}
}.padding()
HStack {
Text(recipe.name == "" ? "New recipe" : recipe.name)
Text(recipe.name == "" ? String(localized: "New recipe") : recipe.name)
.font(.title)
.bold()
.padding()
@@ -132,28 +134,24 @@ struct RecipeEditView: View {
*/
Section() {
NavigationLink(recipe.recipeCategory == "" ? "Category" : "Category: \(recipe.recipeCategory)") {
CategoryPickerView(title: "Category", searchSuggestions: [], selection: $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: [
Keyword("Hauptspeisen"),
Keyword("Lecker"),
Keyword("Trinken"),
Keyword("Essen"),
Keyword("Nachspeisen"),
Keyword("Futter"),
Keyword("Apfel"),
Keyword("test")
],
searchSuggestions: keywordSuggestions,
selection: $keywords
)
}
} header: {
Text("Discoverability")
} footer: {
ScrollView(.horizontal) {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(keywords, id: \.self) { keyword in
Text(keyword)
@@ -174,11 +172,15 @@ struct RecipeEditView: View {
DatePicker("Total time:", selection: $times[2], displayedComponents: .hourAndMinute)
}
EditableListSection(title: "Ingredients", items: $recipe.recipeIngredient)
EditableListSection(title: "Tools", items: $recipe.tool)
EditableListSection(title: "Instructions", items: $recipe.recipeInstructions)
EditableListSection(title: String(localized: "Ingredients"), items: $recipe.recipeIngredient)
EditableListSection(title: String(localized: "Tools"), items: $recipe.tool)
EditableListSection(title: String(localized: "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()
}
}