From 02118e3d7a0da6de3e6ce11b8ccd99b826c930ec Mon Sep 17 00:00:00 2001 From: Hendrik Hogertz Date: Sun, 15 Feb 2026 06:31:14 +0100 Subject: [PATCH] Add dark mode support with appearance picker and fix hardcoded colors Add user-facing appearance setting (System/Light/Dark) wired via preferredColorScheme at the app root. Replace hardcoded .black tints and foreground styles with .primary so toolbar buttons and text remain visible in dark mode. Remove profile picture from settings and SwiftSoup from acknowledgements. Co-Authored-By: Claude Sonnet 4.5 --- .gitignore | 3 + .../project.pbxproj | 4 + .../Data/AppearanceMode.swift | 22 ++++ .../Data/UserSettings.swift | 11 +- .../Localizable.xcstrings | 113 ++++++++++++++++++ .../Nextcloud_Cookbook_iOS_ClientApp.swift | 12 +- .../Views/Recipes/AllRecipesListView.swift | 2 +- .../Views/Recipes/RecipeListView.swift | 2 +- .../Views/SettingsView.swift | 55 ++------- .../Views/Tabs/GroceryListTabView.swift | 2 +- .../Views/Tabs/MealPlanTabView.swift | 2 +- .../Views/Tabs/RecipeTabView.swift | 2 +- 12 files changed, 177 insertions(+), 53 deletions(-) create mode 100644 .gitignore create mode 100644 Nextcloud Cookbook iOS Client/Data/AppearanceMode.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd6e1ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/hendrik.hogertz.xcuserdatad/UserInterfaceState.xcuserstate +Nextcloud Cookbook iOS Client.xcodeproj/xcuserdata/hendrik.hogertz.xcuserdatad/xcschemes/xcschememanagement.plist diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj index f6ccd62..e993896 100644 --- a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj +++ b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj @@ -69,6 +69,7 @@ C1F0AB022D0B000100000001 /* ImportURLSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F0AB012D0B000100000001 /* ImportURLSheet.swift */; }; A9E78A2B2BE7799F00206866 /* JsonAny.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E78A2A2BE7799F00206866 /* JsonAny.swift */; }; A9FA2AB62B5079B200A43702 /* alarm_sound_0.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = A9FA2AB52B5079B200A43702 /* alarm_sound_0.mp3 */; }; + A1B2C3D52F0A000100000001 /* AppearanceMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42F0A000100000001 /* AppearanceMode.swift */; }; D1A0CE012D0A000100000001 /* GroceryListMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A0CE002D0A000100000001 /* GroceryListMode.swift */; }; D1A0CE032D0A000200000002 /* RemindersGroceryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A0CE022D0A000200000002 /* RemindersGroceryStore.swift */; }; D1A0CE052D0A000300000003 /* GroceryListManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A0CE042D0A000300000003 /* GroceryListManager.swift */; }; @@ -165,6 +166,7 @@ A9DA25D42B82096B0061FC2B /* Nextcloud-Cookbook-iOS-Client-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Nextcloud-Cookbook-iOS-Client-Info.plist"; sourceTree = SOURCE_ROOT; }; A9E78A2A2BE7799F00206866 /* JsonAny.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonAny.swift; sourceTree = ""; }; A9FA2AB52B5079B200A43702 /* alarm_sound_0.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = alarm_sound_0.mp3; sourceTree = ""; }; + A1B2C3D42F0A000100000001 /* AppearanceMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceMode.swift; sourceTree = ""; }; D1A0CE002D0A000100000001 /* GroceryListMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroceryListMode.swift; sourceTree = ""; }; D1A0CE022D0A000200000002 /* RemindersGroceryStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemindersGroceryStore.swift; sourceTree = ""; }; D1A0CE042D0A000300000003 /* GroceryListManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroceryListManager.swift; sourceTree = ""; }; @@ -312,6 +314,7 @@ A70171C52AB4C43A00064C43 /* DataModels.swift */, A97B4D312B80B3E900EC1A88 /* RecipeModels.swift */, A9BBB38F2B91BE31002DA7FF /* ObservableRecipeDetail.swift */, + A1B2C3D42F0A000100000001 /* AppearanceMode.swift */, D1A0CE002D0A000100000001 /* GroceryListMode.swift */, D1A0CE022D0A000200000002 /* RemindersGroceryStore.swift */, D1A0CE042D0A000300000003 /* GroceryListManager.swift */, @@ -656,6 +659,7 @@ A76B8A6F2ADFFA8800096CEC /* SupportedLanguage.swift in Sources */, A977D0E02B600318009783A9 /* RecipeTabView.swift in Sources */, A703226F2ABB1DD700D7C4ED /* ColorExtension.swift in Sources */, + A1B2C3D52F0A000100000001 /* AppearanceMode.swift in Sources */, D1A0CE012D0A000100000001 /* GroceryListMode.swift in Sources */, D1A0CE032D0A000200000002 /* RemindersGroceryStore.swift in Sources */, D1A0CE052D0A000300000003 /* GroceryListManager.swift in Sources */, diff --git a/Nextcloud Cookbook iOS Client/Data/AppearanceMode.swift b/Nextcloud Cookbook iOS Client/Data/AppearanceMode.swift new file mode 100644 index 0000000..fceb0aa --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Data/AppearanceMode.swift @@ -0,0 +1,22 @@ +// +// AppearanceMode.swift +// Nextcloud Cookbook iOS Client +// + +import Foundation + +enum AppearanceMode: String, CaseIterable { + case system = "system" + case light = "light" + case dark = "dark" + + func descriptor() -> String { + switch self { + case .system: return String(localized: "System") + case .light: return String(localized: "Light") + case .dark: return String(localized: "Dark") + } + } + + static let allValues: [AppearanceMode] = AppearanceMode.allCases +} diff --git a/Nextcloud Cookbook iOS Client/Data/UserSettings.swift b/Nextcloud Cookbook iOS Client/Data/UserSettings.swift index b978aa0..657d2a2 100644 --- a/Nextcloud Cookbook iOS Client/Data/UserSettings.swift +++ b/Nextcloud Cookbook iOS Client/Data/UserSettings.swift @@ -144,7 +144,13 @@ class UserSettings: ObservableObject { UserDefaults.standard.set(mealPlanSyncEnabled, forKey: "mealPlanSyncEnabled") } } - + + @Published var appearanceMode: String { + didSet { + UserDefaults.standard.set(appearanceMode, forKey: "appearanceMode") + } + } + init() { self.username = UserDefaults.standard.object(forKey: "username") as? String ?? "" self.token = UserDefaults.standard.object(forKey: "token") as? String ?? "" @@ -168,7 +174,8 @@ class UserSettings: ObservableObject { self.remindersListIdentifier = UserDefaults.standard.object(forKey: "remindersListIdentifier") as? String ?? "" self.grocerySyncEnabled = UserDefaults.standard.object(forKey: "grocerySyncEnabled") as? Bool ?? true self.mealPlanSyncEnabled = UserDefaults.standard.object(forKey: "mealPlanSyncEnabled") as? Bool ?? true - + self.appearanceMode = UserDefaults.standard.object(forKey: "appearanceMode") as? String ?? AppearanceMode.system.rawValue + if authString == "" { if token != "" && username != "" { let loginString = "\(self.username):\(self.token)" diff --git a/Nextcloud Cookbook iOS Client/Localizable.xcstrings b/Nextcloud Cookbook iOS Client/Localizable.xcstrings index 73ccf28..0966c6a 100644 --- a/Nextcloud Cookbook iOS Client/Localizable.xcstrings +++ b/Nextcloud Cookbook iOS Client/Localizable.xcstrings @@ -694,6 +694,7 @@ } }, "An HTML parsing and web scraping library for Swift. Used for importing schema.org recipes from websites." : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -782,6 +783,28 @@ } } }, + "Appearance" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erscheinungsbild" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apariencia" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apparence" + } + } + } + }, "Apple Reminders" : { "localizations" : { "de" : { @@ -995,6 +1018,28 @@ } } }, + "Choose whether the app follows the system appearance or always uses light or dark mode." : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wähle, ob die App dem Erscheinungsbild des Systems folgt oder immer den hellen oder dunklen Modus verwendet." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Elige si la app sigue la apariencia del sistema o siempre usa el modo claro u oscuro." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Choisissez si l'app suit l'apparence du système ou utilise toujours le mode clair ou sombre." + } + } + } + }, "Clear" : { "localizations" : { "de" : { @@ -1293,6 +1338,28 @@ } } }, + "Dark" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dunkel" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oscuro" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sombre" + } + } + } + }, "Decimal number format" : { "localizations" : { "de" : { @@ -2536,6 +2603,28 @@ } } }, + "Light" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hell" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Claro" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clair" + } + } + } + }, "List your tools here. 🍴" : { "extractionState" : "stale", "localizations" : { @@ -4460,6 +4549,7 @@ } }, "SwiftSoup" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -4483,6 +4573,28 @@ }, "Sync grocery list across devices" : { + }, + "System" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "System" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sistema" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Système" + } + } + } }, "Thank you for downloading" : { "localizations" : { @@ -5170,6 +5282,7 @@ } }, "Username: %@" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { diff --git a/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift b/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift index 430b325..dcf6168 100644 --- a/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift +++ b/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift @@ -13,7 +13,16 @@ import SwiftUI struct Nextcloud_Cookbook_iOS_ClientApp: App { @AppStorage("onboarding") var onboarding = true @AppStorage("language") var language = Locale.current.language.languageCode?.identifier ?? "en" - + @AppStorage("appearanceMode") var appearanceMode = AppearanceMode.system.rawValue + + var colorScheme: ColorScheme? { + switch appearanceMode { + case AppearanceMode.light.rawValue: return .light + case AppearanceMode.dark.rawValue: return .dark + default: return nil + } + } + var body: some Scene { WindowGroup { ZStack { @@ -23,6 +32,7 @@ struct Nextcloud_Cookbook_iOS_ClientApp: App { MainView() } } + .preferredColorScheme(colorScheme) .transition(.slide) .environment( \.locale, diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/AllRecipesListView.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/AllRecipesListView.swift index e426680..af24122 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/AllRecipesListView.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/AllRecipesListView.swift @@ -56,7 +56,7 @@ struct AllRecipesListView: View { .bold() } .buttonStyle(.bordered) - .tint(.nextcloudBlue) + .tint(.primary) }.padding() } } diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeListView.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeListView.swift index 0cf1f59..9e8cec0 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeListView.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeListView.swift @@ -68,7 +68,7 @@ struct RecipeListView: View { .bold() } .buttonStyle(.bordered) - .tint(.nextcloudBlue) + .tint(.primary) }.padding() } } diff --git a/Nextcloud Cookbook iOS Client/Views/SettingsView.swift b/Nextcloud Cookbook iOS Client/Views/SettingsView.swift index 6067d5d..0fc54d4 100644 --- a/Nextcloud Cookbook iOS Client/Views/SettingsView.swift +++ b/Nextcloud Cookbook iOS Client/Views/SettingsView.swift @@ -22,30 +22,6 @@ struct SettingsView: View { var body: some View { Form { - HStack(alignment: .center) { - if let avatarImage = viewModel.avatarImage { - Image(uiImage: avatarImage) - .resizable() - .clipShape(Circle()) - .frame(width: 100, height: 100) - - } - if let userData = viewModel.userData { - VStack(alignment: .leading) { - Text(userData.userDisplayName) - .font(.title) - .padding(.leading) - Text("Username: \(userData.userId)") - .font(.subheadline) - .padding(.leading) - - - // TODO: Add actions - } - } - Spacer() - } - Section { Picker("Select a default cookbook", selection: $userSettings.defaultCategory) { Text("None").tag("None") @@ -59,6 +35,16 @@ struct SettingsView: View { Text("The selected cookbook will open on app launch by default.") } + Section { + Picker("Appearance", selection: $userSettings.appearanceMode) { + ForEach(AppearanceMode.allValues, id: \.self) { mode in + Text(mode.descriptor()).tag(mode.rawValue) + } + } + } footer: { + Text("Choose whether the app follows the system appearance or always uses light or dark mode.") + } + Section { Picker("Grocery list storage", selection: $userSettings.groceryListMode) { ForEach(GroceryListMode.allValues, id: \.self) { mode in @@ -211,13 +197,6 @@ struct SettingsView: View { } Section(header: Text("Acknowledgements")) { - VStack(alignment: .leading) { - if let url = URL(string: "https://github.com/scinfu/SwiftSoup") { - Link("SwiftSoup", destination: url) - .font(.headline) - Text("An HTML parsing and web scraping library for Swift. Used for importing schema.org recipes from websites.") - } - } VStack(alignment: .leading) { if let url = URL(string: "https://github.com/techprimate/TPPDF") { Link("TPPDF", destination: url) @@ -240,7 +219,6 @@ struct SettingsView: View { Text(viewModel.alertType.getMessage()) } .task { - await viewModel.getUserData() remindersPermission = groceryListManager.remindersPermissionStatus if remindersPermission == .fullAccess { reminderLists = groceryListManager.availableReminderLists() @@ -270,9 +248,6 @@ struct SettingsView: View { extension SettingsView { class ViewModel: ObservableObject { - @Published var avatarImage: UIImage? = nil - @Published var userData: UserData? = nil - @Published var showAlert: Bool = false fileprivate var alertType: SettingsAlert = .NONE @@ -297,16 +272,6 @@ extension SettingsView { } } } - - func getUserData() async { - let (data, _) = await NextcloudApi.getAvatar() - let (userData, _) = await NextcloudApi.getHoverCard() - - DispatchQueue.main.async { - self.avatarImage = data - self.userData = userData - } - } } } diff --git a/Nextcloud Cookbook iOS Client/Views/Tabs/GroceryListTabView.swift b/Nextcloud Cookbook iOS Client/Views/Tabs/GroceryListTabView.swift index 4936bf7..12288c2 100644 --- a/Nextcloud Cookbook iOS Client/Views/Tabs/GroceryListTabView.swift +++ b/Nextcloud Cookbook iOS Client/Views/Tabs/GroceryListTabView.swift @@ -55,7 +55,7 @@ struct GroceryListTabView: View { groceryList.deleteAll() } label: { Text("Delete") - .foregroundStyle(Color.nextcloudBlue) + .foregroundStyle(.primary) } } } diff --git a/Nextcloud Cookbook iOS Client/Views/Tabs/MealPlanTabView.swift b/Nextcloud Cookbook iOS Client/Views/Tabs/MealPlanTabView.swift index 31d64b6..2a39b0d 100644 --- a/Nextcloud Cookbook iOS Client/Views/Tabs/MealPlanTabView.swift +++ b/Nextcloud Cookbook iOS Client/Views/Tabs/MealPlanTabView.swift @@ -191,7 +191,7 @@ fileprivate struct MealPlanDayRow: View { .frame(maxWidth: .infinity, minHeight: 44) .background( RoundedRectangle(cornerRadius: 8) - .fill(Color.nextcloudBlue.opacity(0.1)) + .fill(Color.nextcloudBlue.opacity(0.15)) ) } } diff --git a/Nextcloud Cookbook iOS Client/Views/Tabs/RecipeTabView.swift b/Nextcloud Cookbook iOS Client/Views/Tabs/RecipeTabView.swift index b729f6c..5b829d1 100644 --- a/Nextcloud Cookbook iOS Client/Views/Tabs/RecipeTabView.swift +++ b/Nextcloud Cookbook iOS Client/Views/Tabs/RecipeTabView.swift @@ -163,7 +163,7 @@ struct RecipeTabView: View { } } } - .tint(.nextcloudBlue) + .tint(.primary) .sheet(isPresented: $viewModel.showImportURLSheet) { ImportURLSheet { recipeDetail in viewModel.navigateToImportedRecipe(recipeDetail: recipeDetail)