diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj index 6b6f354..24f102b 100644 --- a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj +++ b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj @@ -25,13 +25,15 @@ A70171C42AB4A31200064C43 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C32AB4A31200064C43 /* DataStore.swift */; }; A70171C62AB4C43A00064C43 /* DataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C52AB4C43A00064C43 /* DataModels.swift */; }; A70171C92AB4CBB400064C43 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C82AB4CBB400064C43 /* OnboardingView.swift */; }; - A70171CB2AB4CD1700064C43 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171CA2AB4CD1700064C43 /* UserDefaults.swift */; }; + A70171CB2AB4CD1700064C43 /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171CA2AB4CD1700064C43 /* UserSettings.swift */; }; A70171CD2AB501B100064C43 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171CC2AB501B100064C43 /* SettingsView.swift */; }; A703226A2ABAF49800D7C4ED /* JSONCoderExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70322692ABAF49800D7C4ED /* JSONCoderExtension.swift */; }; A703226D2ABAF90D00D7C4ED /* APIController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A703226C2ABAF90D00D7C4ED /* APIController.swift */; }; 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 */; }; + A76B8A6F2ADFFA8800096CEC /* SupportedLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76B8A6E2ADFFA8800096CEC /* SupportedLanguage.swift */; }; + A76B8A712AE002AE00096CEC /* AlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76B8A702AE002AE00096CEC /* AlertHandler.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 */; }; @@ -77,13 +79,15 @@ A70171C32AB4A31200064C43 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = ""; }; A70171C52AB4C43A00064C43 /* DataModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataModels.swift; sourceTree = ""; }; A70171C82AB4CBB400064C43 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; - A70171CA2AB4CD1700064C43 /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = ""; }; + A70171CA2AB4CD1700064C43 /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = ""; }; A70171CC2AB501B100064C43 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; A70322692ABAF49800D7C4ED /* JSONCoderExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONCoderExtension.swift; sourceTree = ""; }; A703226C2ABAF90D00D7C4ED /* APIController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIController.swift; sourceTree = ""; }; 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 = ""; }; + A76B8A6E2ADFFA8800096CEC /* SupportedLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedLanguage.swift; sourceTree = ""; }; + A76B8A702AE002AE00096CEC /* AlertHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertHandler.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 = ""; }; @@ -138,13 +142,15 @@ isa = PBXGroup; children = ( A70171812AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift */, + A76B8A702AE002AE00096CEC /* AlertHandler.swift */, + A76B8A6E2ADFFA8800096CEC /* SupportedLanguage.swift */, A70171C72AB4C4A100064C43 /* Data */, A70171BA2AB4980100064C43 /* Views */, A70171B72AB2445700064C43 /* ViewModels */, A70171B22AB211F000064C43 /* Network */, A703226B2ABAF60D00D7C4ED /* Extensions */, - A70171852AA8E71F00064C43 /* Assets.xcassets */, A7AEAE632AD5521400135378 /* Localizable.xcstrings */, + A70171852AA8E71F00064C43 /* Assets.xcassets */, A70171872AA8E71F00064C43 /* Nextcloud_Cookbook_iOS_Client.entitlements */, A70171882AA8E71F00064C43 /* Preview Content */, ); @@ -216,7 +222,7 @@ children = ( A70171C32AB4A31200064C43 /* DataStore.swift */, A70171C52AB4C43A00064C43 /* DataModels.swift */, - A70171CA2AB4CD1700064C43 /* UserDefaults.swift */, + A70171CA2AB4CD1700064C43 /* UserSettings.swift */, ); path = Data; sourceTree = ""; @@ -368,6 +374,7 @@ A70D7CA12AC73CA800D53DBF /* RecipeEditView.swift in Sources */, A70D7CA32AC74B3B00D53DBF /* DateExtension.swift in Sources */, A70171B12AB211DF00064C43 /* CustomError.swift in Sources */, + A76B8A712AE002AE00096CEC /* AlertHandler.swift in Sources */, A70171C42AB4A31200064C43 /* DataStore.swift in Sources */, A70171AF2AB2116B00064C43 /* NetworkHandler.swift in Sources */, A70171B42AB2122900064C43 /* NetworkRequests.swift in Sources */, @@ -380,11 +387,12 @@ A70171CD2AB501B100064C43 /* SettingsView.swift in Sources */, A70171C22AB498C600064C43 /* RecipeCardView.swift in Sources */, A70171842AA8E71900064C43 /* MainView.swift in Sources */, - A70171CB2AB4CD1700064C43 /* UserDefaults.swift in Sources */, + A70171CB2AB4CD1700064C43 /* UserSettings.swift in Sources */, A703226A2ABAF49800D7C4ED /* JSONCoderExtension.swift in Sources */, A703226D2ABAF90D00D7C4ED /* APIController.swift in Sources */, A70171822AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift in Sources */, A70171AD2AA8EF4700064C43 /* MainViewModel.swift in Sources */, + A76B8A6F2ADFFA8800096CEC /* SupportedLanguage.swift in Sources */, A70171C92AB4CBB400064C43 /* OnboardingView.swift in Sources */, A703226F2ABB1DD700D7C4ED /* ColorExtension.swift in Sources */, ); @@ -562,7 +570,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.2; + MARKETING_VERSION = 1.3; PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; @@ -603,7 +611,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.2; + MARKETING_VERSION = 1.3; PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; diff --git a/Nextcloud Cookbook iOS Client/AlertHandler.swift b/Nextcloud Cookbook iOS Client/AlertHandler.swift new file mode 100644 index 0000000..462537a --- /dev/null +++ b/Nextcloud Cookbook iOS Client/AlertHandler.swift @@ -0,0 +1,99 @@ +// +// AlertHandler.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 18.10.23. +// + +import Foundation +import SwiftUI + + + +class AlertHandler: ObservableObject { + @Published var presentAlert: Bool = false + var alert: AlertType = .GENERIC + var alertAction: () -> () = {} + + func present(alert: AlertType, onConfirm: @escaping () -> () = {}) { + self.alert = alert + self.alertAction = onConfirm + self.presentAlert = true + } + + func dismiss() { + self.alertAction = {} + self.alert = .GENERIC + } +} + + + +enum AlertButton: LocalizedStringKey, Identifiable { + var id: Self { + return self + } + + case OK = "Ok", DELETE = "Delete", CANCEL = "Cancel" +} + + + +enum AlertType: Error { + + case NO_TITLE, + DUPLICATE, + UPLOAD_ERROR, + CONFIRM_DELETE, + LOGIN_FAILED, + GENERIC, + CUSTOM(title: LocalizedStringKey, description: LocalizedStringKey) + + var localizedDescription: LocalizedStringKey { + switch self { + case .NO_TITLE: + return "Please enter a recipe name." + case .DUPLICATE: + return "A recipe with that name already exists." + case .UPLOAD_ERROR: + return "Unable to upload your recipe. Please check your internet connection." + case .CONFIRM_DELETE: + return "This action is not reversible!" + case .LOGIN_FAILED: + return "Please check your credentials and internet connection." + case .CUSTOM(title: _, description: let description): + return description + default: + return "An unknown error occured." + } + } + + var localizedTitle: LocalizedStringKey { + switch self { + case .NO_TITLE: + return "Missing recipe name." + case .DUPLICATE: + return "Duplicate recipe." + case .UPLOAD_ERROR: + return "Network error." + case .CONFIRM_DELETE: + return "Delete recipe?" + case .LOGIN_FAILED: + return "Login failed." + case .CUSTOM(title: let title, description: _): + return title + default: + return "Error." + } + } + + var alertButtons: [AlertButton] { + switch self { + case .CONFIRM_DELETE: + return [.CANCEL, .DELETE] + default: + return [.OK] + } + } +} + diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/cookbook-category.imageset/Contents.json b/Nextcloud Cookbook iOS Client/Assets.xcassets/cookbook-category.imageset/Contents.json deleted file mode 100644 index 153e597..0000000 --- a/Nextcloud Cookbook iOS Client/Assets.xcassets/cookbook-category.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "cookbook-category.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/cookbook-category.imageset/cookbook-category.png b/Nextcloud Cookbook iOS Client/Assets.xcassets/cookbook-category.imageset/cookbook-category.png deleted file mode 100644 index 1219d3a..0000000 Binary files a/Nextcloud Cookbook iOS Client/Assets.xcassets/cookbook-category.imageset/cookbook-category.png and /dev/null differ diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/cookbook-recipe.imageset/Contents.json b/Nextcloud Cookbook iOS Client/Assets.xcassets/cookbook-recipe.imageset/Contents.json deleted file mode 100644 index d7eafb9..0000000 --- a/Nextcloud Cookbook iOS Client/Assets.xcassets/cookbook-recipe.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "cookbook-recipe.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/cookbook-recipe.imageset/cookbook-recipe.png b/Nextcloud Cookbook iOS Client/Assets.xcassets/cookbook-recipe.imageset/cookbook-recipe.png deleted file mode 100644 index 0d8a5ca..0000000 Binary files a/Nextcloud Cookbook iOS Client/Assets.xcassets/cookbook-recipe.imageset/cookbook-recipe.png and /dev/null differ diff --git a/Nextcloud Cookbook iOS Client/Data/UserDefaults.swift b/Nextcloud Cookbook iOS Client/Data/UserSettings.swift similarity index 82% rename from Nextcloud Cookbook iOS Client/Data/UserDefaults.swift rename to Nextcloud Cookbook iOS Client/Data/UserSettings.swift index 31f8271..eb98939 100644 --- a/Nextcloud Cookbook iOS Client/Data/UserDefaults.swift +++ b/Nextcloud Cookbook iOS Client/Data/UserSettings.swift @@ -1,5 +1,5 @@ // -// UserDefaults.swift +// UserSettings.swift // Nextcloud Cookbook iOS Client // // Created by Vincent Meilinger on 15.09.23. @@ -46,12 +46,19 @@ class UserSettings: ObservableObject { } } + @Published var downloadRecipes: Bool { + didSet { + UserDefaults.standard.set(downloadRecipes, forKey: "downloadRecipes") + } + } + 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 + self.language = UserDefaults.standard.object(forKey: "language") as? String ?? SupportedLanguage.DEVICE.rawValue + self.downloadRecipes = UserDefaults.standard.object(forKey: "downloadRecipes") as? Bool ?? false } } diff --git a/Nextcloud Cookbook iOS Client/Extensions/ColorExtension.swift b/Nextcloud Cookbook iOS Client/Extensions/ColorExtension.swift index dce32f7..7f55d8d 100644 --- a/Nextcloud Cookbook iOS Client/Extensions/ColorExtension.swift +++ b/Nextcloud Cookbook iOS Client/Extensions/ColorExtension.swift @@ -15,4 +15,7 @@ extension Color { public static var backgroundHighlight: Color { return Color("backgroundHighlight") } + public static var background: Color { + return Color(UIColor.systemBackground) + } } diff --git a/Nextcloud Cookbook iOS Client/Localizable.xcstrings b/Nextcloud Cookbook iOS Client/Localizable.xcstrings index 8a70fae..ec2c01d 100644 --- a/Nextcloud Cookbook iOS Client/Localizable.xcstrings +++ b/Nextcloud Cookbook iOS Client/Localizable.xcstrings @@ -177,6 +177,22 @@ } } }, + "All" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Todas" + } + } + } + }, "An unknown error occured." : { "localizations" : { "de" : { @@ -257,18 +273,18 @@ } } }, - "Cook time" : { + "Connected to server." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Kochen" + "value" : "Verbindung mit dem Server hergestellt." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Duración de cocción" + "value" : "Conexión al servidor establecida." } } } @@ -305,6 +321,22 @@ } } }, + "Cooking" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kochen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cocción" + } + } + } + }, "Cooking duration:" : { "localizations" : { "de" : { @@ -545,6 +577,22 @@ } } }, + "If 'Same as Device' is selected and your device language is not supported yet, this option will default to english." : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wenn \"Systemsprache\" ausgewählt ist und Ihre Systemsprache noch nicht unterstützt wird, wird standardmäßig Englisch verwendet." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si se selecciona 'Idioma del sistema' y el idioma de su dispositivo no es compatible aún, esta opción se establecerá por defecto en inglés." + } + } + } + }, "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" : { @@ -689,6 +737,22 @@ } } }, + "Login failed." : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Login fehlgeschlagen." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inicio de sesión fallido" + } + } + } + }, "Login Method" : { "localizations" : { "de" : { @@ -833,6 +897,22 @@ } } }, + "Please check your credentials and internet connection." : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bitte überprüfen Sie Ihre Anmeldedaten oder Ihre Internetverbindung." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, comprueba tus credenciales de inicio de sesión y la conexión a Internet." + } + } + } + }, "Please enter a recipe name." : { "localizations" : { "de" : { @@ -849,7 +929,7 @@ } } }, - "Prep time" : { + "Preparation" : { "localizations" : { "de" : { "stringUnit" : { @@ -860,7 +940,7 @@ "es" : { "stringUnit" : { "state" : "translated", - "value" : "Duración de preparación" + "value" : "Preparación" } } } @@ -881,6 +961,38 @@ } } }, + "Same as Device" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Systemsprache" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Idioma del sistema" + } + } + } + }, + "Search recipe" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rezept suchen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Buscar receta" + } + } + } + }, "Search recipes" : { "localizations" : { "de" : { @@ -1121,6 +1233,22 @@ } } }, + "Unable to connect to server." : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbindung mit dem Server fehlgeschlagen." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se puede conectar al servidor." + } + } + } + }, "Unable to upload your recipe. Please check your internet connection." : { "localizations" : { "de" : { diff --git a/Nextcloud Cookbook iOS Client/Network/NetworkRequests.swift b/Nextcloud Cookbook iOS Client/Network/NetworkRequests.swift index 9085e41..690b435 100644 --- a/Nextcloud Cookbook iOS Client/Network/NetworkRequests.swift +++ b/Nextcloud Cookbook iOS Client/Network/NetworkRequests.swift @@ -20,6 +20,7 @@ enum RequestPath { RECIPE_DETAIL(recipeId: Int), NEW_RECIPE, IMAGE(recipeId: Int, thumb: Bool), + CONFIG, KEYWORDS case LOGINV2REQ, @@ -33,6 +34,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 .CONFIG: return "config" case .KEYWORDS: return "keywords" case .LOGINV2REQ: return "/index.php/login/v2" diff --git a/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift b/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift index d089e69..55a9174 100644 --- a/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift +++ b/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift @@ -11,6 +11,7 @@ import SwiftUI struct Nextcloud_Cookbook_iOS_ClientApp: App { @StateObject var userSettings = UserSettings() @StateObject var mainViewModel = MainViewModel() + @StateObject var alertHandler = AlertHandler() var body: some Scene { WindowGroup { @@ -25,8 +26,12 @@ struct Nextcloud_Cookbook_iOS_ClientApp: App { } } .transition(.slide) - .environment(\.locale, .init(identifier: userSettings.language)) + .environment( + \.locale, + .init(identifier: userSettings.language == + SupportedLanguage.DEVICE.rawValue ? (Locale.current.language.languageCode?.identifier ?? "en") : userSettings.language) + ) + .environmentObject(alertHandler) } - } } diff --git a/Nextcloud Cookbook iOS Client/SupportedLanguage.swift b/Nextcloud Cookbook iOS Client/SupportedLanguage.swift new file mode 100644 index 0000000..9b5daf9 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/SupportedLanguage.swift @@ -0,0 +1,31 @@ +// +// SupportedLanguage.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 18.10.23. +// + +import Foundation + + +enum SupportedLanguage: String, Codable { + case DEVICE = "device", + EN = "en", + DE = "de", + ES = "es" + + func descriptor() -> String { + switch self { + case .DEVICE: + return String(localized: "Same as Device") + case .EN: + return "English" + case .DE: + return "Deutsch" + case .ES: + return "Español" + } + } + + static let allValues = [DEVICE, EN, DE, ES] +} diff --git a/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift b/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift index f1ccd11..65c22b1 100644 --- a/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift +++ b/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift @@ -64,6 +64,19 @@ import SwiftUI } + func getAllRecipes() async -> [Recipe] { + var allRecipes: [Recipe] = [] + for category in categories { + await loadRecipeList(categoryName: category.name) + if let recipeArray = recipes[category.name] { + allRecipes.append(contentsOf: recipeArray) + } + } + return allRecipes.sorted(by: { + $0.name < $1.name + }) + } + /// Try to load the recipe details from cache. If not found, try to load from store or the server. /// - Parameters /// - recipeId: The id of the recipe. @@ -204,6 +217,22 @@ import SwiftUI }) recipeDetails.removeValue(forKey: id) } + + func checkServerConnection() async -> Bool { + guard let apiController = apiController else { return false } + let req = RequestWrapper.customRequest( + method: .GET, + path: .CONFIG, + headerFields: [ + .ocsRequest(value: true), + .accept(value: .JSON) + ] + ) + if let error = await apiController.sendRequest(req) { + return false + } + return true + } } @@ -225,7 +254,7 @@ extension MainViewModel { } return data } - }catch { + } catch { print("An unknown error occurred.") } return nil diff --git a/Nextcloud Cookbook iOS Client/Views/MainView.swift b/Nextcloud Cookbook iOS Client/Views/MainView.swift index a2d12d5..5061e6f 100644 --- a/Nextcloud Cookbook iOS Client/Views/MainView.swift +++ b/Nextcloud Cookbook iOS Client/Views/MainView.swift @@ -14,104 +14,203 @@ struct MainView: View { @State private var selectedCategory: Category? = nil @State private var showEditView: Bool = false + @State private var showSearchView: Bool = false @State private var showSettingsView: Bool = false + @State private var serverConnection: Bool = false + var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 0)] var body: some View { NavigationSplitView { - List(viewModel.categories, selection: $selectedCategory) { category in - if category.recipe_count != 0 { - NavigationLink(value: category) { - HStack(alignment: .center) { - Image(systemName: "book.closed.fill") - Text(category.name == "*" ? "Other" : category.name) - .font(.system(size: 20, weight: .light, design: .serif)) - .italic() - }.padding(7) + List(selection: $selectedCategory) { + // All recipes + NavigationLink { + RecipeSearchView(viewModel: viewModel) + } label: { + HStack(alignment: .center) { + Image(systemName: "book.closed.fill") + Text("All") + .font(.system(size: 20, weight: .light, design: .serif)) + .italic() + } + .padding(7) + } + + // Categories + ForEach(viewModel.categories) { category in + if category.recipe_count != 0 { + NavigationLink(value: category) { + HStack(alignment: .center) { + Image(systemName: "book.closed.fill") + Text(category.name == "*" ? "Other" : category.name) + .font(.system(size: 20, weight: .light, design: .serif)) + .italic() + }.padding(7) + } } } } + .navigationTitle("Cookbooks") .navigationDestination(isPresented: $showSettingsView) { SettingsView(userSettings: userSettings, viewModel: viewModel) } + .navigationDestination(isPresented: $showSearchView) { + RecipeSearchView(viewModel: viewModel) + } .toolbar { - ToolbarItem(placement: .topBarLeading) { - Menu { - Button { - print("Downloading all recipes ...") - Task { - await viewModel.downloadAllRecipes() - } - } label: { - HStack { - Text("Download all recipes") - Image(systemName: "icloud.and.arrow.down") - } - } - - Button { - self.showSettingsView = true - } label: { - Text("Settings") - Image(systemName: "gearshape") - } - } label: { - Image(systemName: "ellipsis.circle") + MainViewToolBar( + viewModel: viewModel, + showEditView: $showEditView, + showSettingsView: $showSettingsView, + serverConnection: $serverConnection + ) + } + } detail: { + NavigationStack { + if let category = selectedCategory { + CategoryDetailView( + categoryName: category.name, + viewModel: viewModel, + showEditView: $showEditView + ) + .id(category.id) // Workaround: This is needed to update the detail view when the selection changes } } - ToolbarItem(placement: .topBarTrailing) { + } + .tint(.nextcloudBlue) + .sheet(isPresented: $showEditView) { + RecipeEditView(viewModel: viewModel, isPresented: $showEditView) + } + .task { + self.serverConnection = await viewModel.checkServerConnection() + await viewModel.loadCategoryList() + // Open detail view for default category + if userSettings.defaultCategory != "" { + if let cat = viewModel.categories.first(where: { c in + if c.name == userSettings.defaultCategory { + return true + } + return false + }) { + self.selectedCategory = cat + } + } + } + .refreshable { + self.serverConnection = await viewModel.checkServerConnection() + await viewModel.loadCategoryList(needsUpdate: true) + } + + } + } + + + + + struct MainViewToolBar: ToolbarContent { + @ObservedObject var viewModel: MainViewModel + @Binding var showEditView: Bool + @Binding var showSettingsView: Bool + @Binding var serverConnection: Bool + @State private var presentPopover: Bool = false + + var body: some ToolbarContent { + // Top left menu toolbar item + ToolbarItem(placement: .topBarLeading) { + Menu { Button { - print("Add new recipe") - showEditView = true + print("Downloading all recipes ...") + Task { + await viewModel.downloadAllRecipes() + } } label: { HStack { - Image(systemName: "plus.circle.fill") + Text("Download all recipes") + Image(systemName: "icloud.and.arrow.down") + } + } + + Button { + self.showSettingsView = true + } label: { + Text("Settings") + Image(systemName: "gearshape") + } + } label: { + Image(systemName: "ellipsis.circle") + } + } + + // Server connection indicator + ToolbarItem(placement: .topBarTrailing) { + Button { + print("Check server connection") + presentPopover = true + } label: { + if serverConnection { + Image(systemName: "checkmark.icloud") + } else { + Image(systemName: "xmark.icloud") + } + }.popover(isPresented: $presentPopover) { + Text(serverConnection ? LocalizedStringKey("Connected to server.") : LocalizedStringKey("Unable to connect to server.")) + .bold() + .padding() + .presentationCompactAdaptation(.popover) + } + } + + // Create new recipes + ToolbarItem(placement: .topBarTrailing) { + Button { + print("Add new recipe") + showEditView = true + } label: { + Image(systemName: "plus.circle.fill") + } + } + + } + } + + + +struct RecipeSearchView: View { + @ObservedObject var viewModel: MainViewModel + @State var searchText: String = "" + @State var allRecipes: [Recipe] = [] + + var body: some View { + NavigationStack { + VStack { + ScrollView(showsIndicators: false) { + LazyVStack { + ForEach(recipesFiltered(), id: \.recipe_id) { recipe in + NavigationLink(value: recipe) { + RecipeCardView(viewModel: viewModel, recipe: recipe) + } + .buttonStyle(.plain) } } } - } - } detail: { - NavigationStack { - if let category = selectedCategory { - CategoryDetailView( - categoryName: category.name, - viewModel: viewModel, - showEditView: $showEditView - ) - .id(category.id) // Workaround: This is needed to update the detail view when the selection changes + .navigationDestination(for: Recipe.self) { recipe in + RecipeDetailView(viewModel: viewModel, recipe: recipe) } + .searchable(text: $searchText, prompt: "Search recipes") } - } - .tint(.nextcloudBlue) - .sheet(isPresented: $showEditView) { - RecipeEditView(viewModel: viewModel, isPresented: $showEditView) + .navigationTitle("Search recipe") } .task { - await viewModel.loadCategoryList() - if userSettings.defaultCategory != "" { - if let cat = viewModel.categories.first(where: { c in - if c.name == userSettings.defaultCategory { - return true - } - return false - }) { - self.selectedCategory = cat - } - } + allRecipes = await viewModel.getAllRecipes() } - .refreshable { - await viewModel.loadCategoryList(needsUpdate: true) + } + + func recipesFiltered() -> [Recipe] { + guard searchText != "" else { return allRecipes } + return allRecipes.filter { recipe in + recipe.name.lowercased().contains(searchText.lowercased()) } } } - - - - - - - - - diff --git a/Nextcloud Cookbook iOS Client/Views/RecipeCardView.swift b/Nextcloud Cookbook iOS Client/Views/RecipeCardView.swift index ab623ac..f5a7fd6 100644 --- a/Nextcloud Cookbook iOS Client/Views/RecipeCardView.swift +++ b/Nextcloud Cookbook iOS Client/Views/RecipeCardView.swift @@ -16,11 +16,24 @@ struct RecipeCardView: View { var body: some View { HStack { - Image(uiImage: recipeThumb ?? UIImage(named: "cookbook-recipe")!) - .resizable() - .aspectRatio(contentMode: .fill) + if let recipeThumb = recipeThumb { + Image(uiImage: recipeThumb) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 80, height: 80) + .clipped() + } else { + ZStack { + Image(systemName: "square.text.square") + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundStyle(Color.white) + .padding(10) + + } + .background(Color("ncblue")) .frame(width: 80, height: 80) - .clipped() + } Text(recipe.name) .font(.headline) diff --git a/Nextcloud Cookbook iOS Client/Views/RecipeEditView.swift b/Nextcloud Cookbook iOS Client/Views/RecipeEditView.swift index 55dcfe1..db00f71 100644 --- a/Nextcloud Cookbook iOS Client/Views/RecipeEditView.swift +++ b/Nextcloud Cookbook iOS Client/Views/RecipeEditView.swift @@ -12,6 +12,7 @@ import PhotosUI struct RecipeEditView: View { + @EnvironmentObject var alertHandler: AlertHandler @ObservedObject var viewModel: MainViewModel @State var recipe: RecipeDetail = RecipeDetail() @Binding var isPresented: Bool @@ -25,8 +26,6 @@ struct RecipeEditView: View { @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 { @@ -43,8 +42,9 @@ struct RecipeEditView: View { Menu { Button { print("Delete recipe.") - alertMessage = .CONFIRM_DELETE - presentAlert = true + alertHandler.present(alert: .CONFIRM_DELETE) { + deleteRecipe() + } } label: { Image(systemName: "trash") .foregroundStyle(.red) @@ -152,18 +152,24 @@ struct RecipeEditView: View { keywords.append(keyword) } } - .alert(alertMessage.localizedTitle, isPresented: $presentAlert) { - switch alertMessage { - case .CONFIRM_DELETE: - Button("Cancel", role: .cancel) { } - Button("Delete", role: .destructive) { - deleteRecipe() + .alert(alertHandler.alert.localizedTitle, isPresented: $alertHandler.presentAlert) { + ForEach(alertHandler.alert.alertButtons) { buttonType in + if buttonType == .OK { + Button(AlertButton.OK.rawValue, role: .cancel) { + alertHandler.alertAction() + alertHandler.dismiss() + } + } else if buttonType == .CANCEL { + Button(AlertButton.CANCEL.rawValue, role: .cancel) { } + } else if buttonType == .DELETE { + Button(AlertButton.DELETE.rawValue, role: .destructive) { + alertHandler.alertAction() + alertHandler.dismiss() + } } - default: - Button("Ok", role: .cancel) { } } } message: { - Text(alertMessage.localizedDescription) + Text(alertHandler.alert.localizedDescription) } } @@ -179,9 +185,8 @@ struct RecipeEditView: View { func recipeValid() -> Bool { // Check if the recipe has a name - if recipe.name == "" { - self.alertMessage = .NO_TITLE - self.presentAlert = true + if recipe.name.replacingOccurrences(of: " ", with: "") == "" { + alertHandler.present(alert: .NO_TITLE) return false } // Check if the recipe has a unique name @@ -194,8 +199,7 @@ struct RecipeEditView: View { .replacingOccurrences(of: " ", with: "") .lowercased() { - self.alertMessage = .DUPLICATE - self.presentAlert = true + alertHandler.present(alert: .DUPLICATE) return false } } @@ -266,11 +270,7 @@ struct RecipeEditView: View { guard let data = data else { return } do { let error = try JSONDecoder().decode(ServerMessage.self, from: data) - DispatchQueue.main.sync { - alertMessage = .CUSTOM(title: "Error.", description: LocalizedStringKey(stringLiteral: error.msg)) - presentAlert = true - return - } + // TODO: Better error handling (Show error to user!) } catch { } @@ -410,46 +410,3 @@ fileprivate class Duration: ObservableObject { -fileprivate enum ErrorMessages: Error { - - case NO_TITLE, - DUPLICATE, - UPLOAD_ERROR, - CONFIRM_DELETE, - GENERIC, - CUSTOM(title: LocalizedStringKey, description: LocalizedStringKey) - - var localizedDescription: LocalizedStringKey { - switch self { - case .NO_TITLE: - return "Please enter a recipe name." - case .DUPLICATE: - return "A recipe with that name already exists." - case .UPLOAD_ERROR: - return "Unable to upload your recipe. Please check your internet connection." - case .CONFIRM_DELETE: - return "This action is not reversible!" - case .CUSTOM(title: _, description: let description): - return description - default: - return "An unknown error occured." - } - } - - var localizedTitle: LocalizedStringKey { - switch self { - case .NO_TITLE: - return "Missing recipe name." - case .DUPLICATE: - return "Duplicate recipe." - case .UPLOAD_ERROR: - return "Network error." - case .CONFIRM_DELETE: - return "Delete recipe?" - case .CUSTOM(title: let title, description: _): - return title - default: - return "Error." - } - } -} diff --git a/Nextcloud Cookbook iOS Client/Views/SettingsView.swift b/Nextcloud Cookbook iOS Client/Views/SettingsView.swift index c5f4a8f..616269e 100644 --- a/Nextcloud Cookbook iOS Client/Views/SettingsView.swift +++ b/Nextcloud Cookbook iOS Client/Views/SettingsView.swift @@ -8,45 +8,7 @@ import Foundation import SwiftUI -fileprivate enum SettingsAlert { - case LOG_OUT, - DELETE_CACHE, - NONE - - func getTitle() -> String { - switch self { - case .LOG_OUT: return "Log out" - case .DELETE_CACHE: return "Delete local data" - default: return "Please confirm your action." - } - } - - func getMessage() -> String { - switch self { - case .LOG_OUT: return "Are you sure that you want to log out of your account?" - case .DELETE_CACHE: return "Are you sure that you want to delete the downloaded recipes? This action will not affect any recipes stored on your server." - default: return "" - } - } -} -enum SupportedLanguage: String, Codable { - case EN = "en", - DE = "de", - ES = "es" - func descriptor() -> String { - switch self { - case .EN: - return "English" - case .DE: - return "Deutsch" - case .ES: - return "Español" - } - } - - static let allValues = [EN, DE, ES] -} struct SettingsView: View { @ObservedObject var userSettings: UserSettings @@ -58,23 +20,32 @@ struct SettingsView: View { var body: some View { Form { Section { + /*Toggle(isOn: $userSettings.downloadRecipes) { + Text("Always download new recipes") + }*/ 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).tag(category) } } - Picker("Language", selection: $userSettings.language) { - ForEach(SupportedLanguage.allValues, id: \.self) { lang in - Text(lang.descriptor()).tag(lang.rawValue) - } - } } header: { Text("General") } footer: { Text("The selected cookbook will open on app launch by default.") } + Section { + Picker("Language", selection: $userSettings.language) { + ForEach(SupportedLanguage.allValues, id: \.self) { lang in + Text(lang.descriptor()).tag(lang.rawValue) + } + } + } footer: { + Text("If \'Same as Device\' is selected and your device language is not supported yet, this option will default to english.") + } + + Section { Link("Visit the GitHub page", destination: URL(string: "https://github.com/VincentMeilinger/Nextcloud-Cookbook-iOS")!) } header: { @@ -142,3 +113,24 @@ struct SettingsView: View { +fileprivate enum SettingsAlert { + case LOG_OUT, + DELETE_CACHE, + NONE + + func getTitle() -> String { + switch self { + case .LOG_OUT: return "Log out" + case .DELETE_CACHE: return "Delete local data" + default: return "Please confirm your action." + } + } + + func getMessage() -> String { + switch self { + case .LOG_OUT: return "Are you sure that you want to log out of your account?" + case .DELETE_CACHE: return "Are you sure that you want to delete the downloaded recipes? This action will not affect any recipes stored on your server." + default: return "" + } + } +}