iOS: per-setting sync table with sync icons, dropdowns, colour resets

Generalise the two-tier SettingsSync into a server-driven flag map: each
syncable key has an account-wide flag (fetched from the server); pull applies
only on-flagged keys, push (read-modify-write) sends only on-flagged keys plus
the flag map. Per-row link icon toggles a setting's sync; a global switch flips
all. Enabling a flag adopts this device's current value as the shared one.

- AppSettings: fix bg_color mapping (was background_color), add cache_months,
  month_view_paged, sync_flags
- SettingsView: uniform rows (sync icon | name | value), ContrastSelector and
  the button pickers replaced by menu dropdowns, every colour gets a reset to a
  canonical default, global "sync all" toggle; account settings (privacy,
  group calendar, hide profile) kept in their own block; language and text/line
  contrast kept device-local (iOS-specific)
- unify default line colour to #3A3A52 across views

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-14 21:31:16 +02:00
parent d4fd7beaf7
commit 9a50f25f57
8 changed files with 468 additions and 431 deletions

View File

@@ -15,11 +15,14 @@ struct AppSettings: Codable {
var monthLabelColor: String = "#7090c0"
var textColor: String = "#FFFFFF"
var backgroundColor: String = "#000000"
var lineColor: String = "#3A3A3C"
var lineColor: String = "#3A3A52"
var privateEventVisibility: String = "busy" // 'hidden' | 'busy'
var groupVisibleCalendarId: Int? = nil
var defaultReminderMinutes: Int? = nil // minutes before start; nil = off
var defaultEventDurationMinutes: Int = 60 // applied to a new event's end time
var cacheMonths: Int = 3 // preloaded month range (device-local by default)
var monthViewPaged: Bool = false // month view swipe-paging (device-local by default)
var syncFlags: [String: Bool]? = nil // server-resolved per-setting sync flags
enum CodingKeys: String, CodingKey {
case defaultView = "default_view"
@@ -35,21 +38,24 @@ struct AppSettings: Codable {
case monthDividerColor = "month_divider_color"
case monthLabelColor = "month_label_color"
case textColor = "text_color"
case backgroundColor = "background_color"
case backgroundColor = "bg_color"
case lineColor = "line_color"
case privateEventVisibility = "private_event_visibility"
case groupVisibleCalendarId = "group_visible_calendar_id"
case defaultReminderMinutes = "default_reminder_minutes"
case defaultEventDurationMinutes = "default_event_duration_minutes"
case cacheMonths = "cache_months"
case monthViewPaged = "month_view_paged"
case syncFlags = "sync_flags"
}
init() {}
/// Resilient decoding: the server only stores a subset of these fields
/// (e.g. it has no `text_color`/`background_color`/`line_color`, which are
/// iOS-only). Using `decodeIfPresent` with the property defaults means a
/// missing key no longer aborts the whole decode otherwise the entire
/// settings sync silently breaks.
/// Resilient decoding: a server may omit some keys. Using `decodeIfPresent`
/// with the property defaults means a missing key no longer aborts the whole
/// decode otherwise the entire settings sync silently breaks. (Note:
/// `background_color` was previously mis-mapped; the server column is
/// `bg_color`, now corrected above.)
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
let d = AppSettings()
@@ -72,6 +78,9 @@ struct AppSettings: Codable {
groupVisibleCalendarId = try c.decodeIfPresent(Int.self, forKey: .groupVisibleCalendarId)
defaultReminderMinutes = try c.decodeIfPresent(Int.self, forKey: .defaultReminderMinutes)
defaultEventDurationMinutes = try c.decodeIfPresent(Int.self, forKey: .defaultEventDurationMinutes) ?? d.defaultEventDurationMinutes
cacheMonths = try c.decodeIfPresent(Int.self, forKey: .cacheMonths) ?? d.cacheMonths
monthViewPaged = try c.decodeIfPresent(Bool.self, forKey: .monthViewPaged) ?? d.monthViewPaged
syncFlags = try c.decodeIfPresent([String: Bool].self, forKey: .syncFlags)
}
}

View File

@@ -579,7 +579,7 @@ class CalendarStore {
todayColorHex: defaults.string(forKey: "todayColor") ?? "#4285f4",
textColorHex: defaults.string(forKey: "textColor") ?? "#FFFFFF",
backgroundColorHex: defaults.string(forKey: "backgroundColor") ?? "#000000",
lineColorHex: defaults.string(forKey: "lineColor") ?? "#3A3A3C",
lineColorHex: defaults.string(forKey: "lineColor") ?? "#3A3A52",
primaryColorHex: defaults.string(forKey: "primaryColor") ?? "#4285f4",
accentColorHex: defaults.string(forKey: "accentColor") ?? "#ea4335",
language: defaults.string(forKey: "appLanguage") ?? "system"

View File

@@ -81,6 +81,13 @@ private let strings: [String: [String: String]] = [
"settings.sync": "Einstellungen synchronisieren",
"settings.sync.desc": "Darstellung mit dem Server abgleichen",
"settings.sync.footer": "Wenn aktiv, werden Farben, Kontraste und Stundenhöhe mit dem Server abgeglichen (der Server hat Vorrang). Ansicht, erster Wochentag und das Ausgrauen vergangener Termine werden immer synchronisiert auch wenn der Schalter aus ist.",
"settings.sync_all": "Alle synchronisieren",
"settings.sync_all.desc": "Diese Einstellungen zwischen deinen Geräten teilen",
"settings.sync_this": "Zwischen Geräten synchronisieren",
"settings.reset": "Zurücksetzen",
"settings.appearance": "Ansicht",
"settings.device": "Nur auf diesem Gerät",
"settings.device.footer": "Diese Einstellungen gelten nur auf diesem Gerät und werden nicht synchronisiert.",
"settings.cache.header": "Vorladen",
"settings.cache.title": "Vorladen",
@@ -440,6 +447,13 @@ private let strings: [String: [String: String]] = [
"settings.sync": "Sync settings",
"settings.sync.desc": "Keep appearance in sync with the server",
"settings.sync.footer": "When on, colors, contrasts and hour height sync with the server (the server wins). View, first weekday and dimming past events always sync even when the switch is off.",
"settings.sync_all": "Sync all",
"settings.sync_all.desc": "Share these settings across your devices",
"settings.sync_this": "Sync across devices",
"settings.reset": "Reset",
"settings.appearance": "View",
"settings.device": "This device only",
"settings.device.footer": "These settings apply to this device only and are not synced.",
"settings.cache.header": "Preloading",
"settings.cache.title": "Preloading",