From c0a63d75606103e60c6edf4197e2867c972eb8c9 Mon Sep 17 00:00:00 2001 From: Vicnet <35202538+VincentMeilinger@users.noreply.github.com> Date: Tue, 10 Oct 2023 13:37:46 +0200 Subject: [PATCH] Keyword suggestions and language support. --- .../project.pbxproj | 9 +- .../Data/DataModels.swift | 5 + .../Data/DataStore.swift | 7 + .../Data/UserDefaults.swift | 7 + .../Localizable.xcstrings | 746 ++++++++++++++++++ .../Network/NetworkRequests.swift | 4 +- .../Nextcloud_Cookbook_iOS_ClientApp.swift | 11 +- .../ViewModels/MainViewModel.swift | 21 + .../Views/CategoryDetailView.swift | 8 +- .../Views/CategoryPickerView.swift | 8 - .../Views/KeywordPickerView.swift | 54 +- .../Views/MainView.swift | 27 +- .../Views/OnboardingView.swift | 4 +- .../Views/RecipeDetailView.swift | 16 +- .../Views/RecipeEditView.swift | 191 ++--- .../Views/SettingsView.swift | 37 +- 16 files changed, 986 insertions(+), 169 deletions(-) create mode 100644 Nextcloud Cookbook iOS Client/Localizable.xcstrings diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj index 4773657..77df786 100644 --- a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj +++ b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj @@ -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 = ""; }; A70D7CA02AC73CA700D53DBF /* RecipeEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeEditView.swift; sourceTree = ""; }; A70D7CA22AC74B3B00D53DBF /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = ""; }; + A7AEAE632AD5521400135378 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; A7F3F8E72ACBFC760076C227 /* KeywordPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordPickerView.swift; sourceTree = ""; }; A7F3F8E92ACC221C0076C227 /* CategoryPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerView.swift; sourceTree = ""; }; /* 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; diff --git a/Nextcloud Cookbook iOS Client/Data/DataModels.swift b/Nextcloud Cookbook iOS Client/Data/DataModels.swift index e26b948..37270ce 100644 --- a/Nextcloud Cookbook iOS Client/Data/DataModels.swift +++ b/Nextcloud Cookbook iOS Client/Data/DataModels.swift @@ -115,6 +115,11 @@ struct RecipeImage { var full: UIImage? } +struct RecipeKeyword: Codable { + let name: String + let recipe_count: Int +} + diff --git a/Nextcloud Cookbook iOS Client/Data/DataStore.swift b/Nextcloud Cookbook iOS Client/Data/DataStore.swift index 0b1525c..66fbd1e 100644 --- a/Nextcloud Cookbook iOS Client/Data/DataStore.swift +++ b/Nextcloud Cookbook iOS Client/Data/DataStore.swift @@ -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 } diff --git a/Nextcloud Cookbook iOS Client/Data/UserDefaults.swift b/Nextcloud Cookbook iOS Client/Data/UserDefaults.swift index 8fdc291..31f8271 100644 --- a/Nextcloud Cookbook iOS Client/Data/UserDefaults.swift +++ b/Nextcloud Cookbook iOS Client/Data/UserDefaults.swift @@ -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 } } diff --git a/Nextcloud Cookbook iOS Client/Localizable.xcstrings b/Nextcloud Cookbook iOS Client/Localizable.xcstrings new file mode 100644 index 0000000..940fa93 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Localizable.xcstrings @@ -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" +} \ No newline at end of file diff --git a/Nextcloud Cookbook iOS Client/Network/NetworkRequests.swift b/Nextcloud Cookbook iOS Client/Network/NetworkRequests.swift index 0e43bfb..9085e41 100644 --- a/Nextcloud Cookbook iOS Client/Network/NetworkRequests.swift +++ b/Nextcloud Cookbook iOS Client/Network/NetworkRequests.swift @@ -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 diff --git a/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift b/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift index 8409cb7..b4b9e5b 100644 --- a/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift +++ b/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift @@ -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)) } + } } diff --git a/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift b/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift index e10e26f..f1ccd11 100644 --- a/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift +++ b/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift @@ -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) + } } diff --git a/Nextcloud Cookbook iOS Client/Views/CategoryDetailView.swift b/Nextcloud Cookbook iOS Client/Views/CategoryDetailView.swift index 71d77cc..302514b 100644 --- a/Nextcloud Cookbook iOS Client/Views/CategoryDetailView.swift +++ b/Nextcloud Cookbook iOS Client/Views/CategoryDetailView.swift @@ -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) { diff --git a/Nextcloud Cookbook iOS Client/Views/CategoryPickerView.swift b/Nextcloud Cookbook iOS Client/Views/CategoryPickerView.swift index 5375afb..551600d 100644 --- a/Nextcloud Cookbook iOS Client/Views/CategoryPickerView.swift +++ b/Nextcloud Cookbook iOS Client/Views/CategoryPickerView.swift @@ -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 } diff --git a/Nextcloud Cookbook iOS Client/Views/KeywordPickerView.swift b/Nextcloud Cookbook iOS Client/Views/KeywordPickerView.swift index 33d2ac3..2a06a36 100644 --- a/Nextcloud Cookbook iOS Client/Views/KeywordPickerView.swift +++ b/Nextcloud Cookbook iOS Client/Views/KeywordPickerView.swift @@ -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( diff --git a/Nextcloud Cookbook iOS Client/Views/MainView.swift b/Nextcloud Cookbook iOS Client/Views/MainView.swift index e2e2a30..0591173 100644 --- a/Nextcloud Cookbook iOS Client/Views/MainView.swift +++ b/Nextcloud Cookbook iOS Client/Views/MainView.swift @@ -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) { diff --git a/Nextcloud Cookbook iOS Client/Views/OnboardingView.swift b/Nextcloud Cookbook iOS Client/Views/OnboardingView.swift index 8b3d4df..ef69d31 100644 --- a/Nextcloud Cookbook iOS Client/Views/OnboardingView.swift +++ b/Nextcloud Cookbook iOS Client/Views/OnboardingView.swift @@ -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() } diff --git a/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift b/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift index ccff280..1b54f5a 100644 --- a/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift +++ b/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift @@ -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.. 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() } }