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 <noreply@anthropic.com>
This commit is contained in:
22
Nextcloud Cookbook iOS Client/Data/AppearanceMode.swift
Normal file
22
Nextcloud Cookbook iOS Client/Data/AppearanceMode.swift
Normal file
@@ -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
|
||||
}
|
||||
@@ -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)"
|
||||
|
||||
@@ -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" : {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -56,7 +56,7 @@ struct AllRecipesListView: View {
|
||||
.bold()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(.nextcloudBlue)
|
||||
.tint(.primary)
|
||||
}.padding()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ struct RecipeListView: View {
|
||||
.bold()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(.nextcloudBlue)
|
||||
.tint(.primary)
|
||||
}.padding()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ struct GroceryListTabView: View {
|
||||
groceryList.deleteAll()
|
||||
} label: {
|
||||
Text("Delete")
|
||||
.foregroundStyle(Color.nextcloudBlue)
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ struct RecipeTabView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.tint(.nextcloudBlue)
|
||||
.tint(.primary)
|
||||
.sheet(isPresented: $viewModel.showImportURLSheet) {
|
||||
ImportURLSheet { recipeDetail in
|
||||
viewModel.navigateToImportedRecipe(recipeDetail: recipeDetail)
|
||||
|
||||
Reference in New Issue
Block a user