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:
@@ -3,31 +3,30 @@ import SwiftUI
|
||||
struct SettingsView: View {
|
||||
let api: CalendarrAPI
|
||||
@AppStorage("liquidGlass") private var liquidGlass = false
|
||||
@AppStorage("settingsSync") private var settingsSync = false
|
||||
@AppStorage("cacheMonths") private var cacheMonths = 3
|
||||
@AppStorage("appLanguage") private var appLang = "system"
|
||||
@AppStorage("monthDividerColor") private var dividerHex = "#7090c0"
|
||||
@AppStorage("monthLabelColor") private var labelHex = "#7090c0"
|
||||
@AppStorage("todayColor") private var todayHex = "#4285f4"
|
||||
@AppStorage("monthDividerColor") private var dividerHex = "#7090C0"
|
||||
@AppStorage("monthLabelColor") private var labelHex = "#7090C0"
|
||||
@AppStorage("todayColor") private var todayHex = "#4285F4"
|
||||
@AppStorage("textColor") private var textHex = "#FFFFFF"
|
||||
@AppStorage("backgroundColor") private var bgHex = "#000000"
|
||||
@AppStorage("lineColor") private var lineHex = "#3A3A3C"
|
||||
@AppStorage("primaryColor") private var primaryHex = "#4285f4"
|
||||
@AppStorage("accentColor") private var accentHex = "#ea4335"
|
||||
// Previously server-only; now AppStorage-backed so they persist and the
|
||||
// calendar views actually apply them.
|
||||
@AppStorage("lineColor") private var lineHex = "#3A3A52"
|
||||
@AppStorage("primaryColor") private var primaryHex = "#4285F4"
|
||||
@AppStorage("accentColor") private var accentHex = "#EA4335"
|
||||
// iOS-only opacity controls (drive secondary text / grid-line opacity in the
|
||||
// calendar views); kept device-local, not part of the cross-device sync.
|
||||
@AppStorage("textContrast") private var textContrast = 3
|
||||
@AppStorage("lineContrast") private var lineContrast = 3
|
||||
@AppStorage("hourHeight") private var hourHeight = 60
|
||||
@AppStorage("defaultView") private var defaultView = "month"
|
||||
@AppStorage("weekStartDay") private var weekStartDay = "monday"
|
||||
@AppStorage("dimPastEvents") private var dimPastEvents = false
|
||||
// Device-local: month view as horizontal paged (swipe) vs. scroll feed.
|
||||
@AppStorage("monthViewPaged") private var monthViewPaged = false
|
||||
@AppStorage("defaultReminderMinutes") private var defaultReminderMinutes = -1
|
||||
@AppStorage("defaultEventDurationMinutes") private var defaultEventDurationMinutes = 60
|
||||
|
||||
// Profile chapter (server-backed; loaded on appear).
|
||||
// Profile chapter (server-backed; loaded on appear). Account settings are not
|
||||
// part of the per-device sync — they are always account-wide.
|
||||
@State private var displayName = ""
|
||||
@State private var loginName = ""
|
||||
@State private var email = ""
|
||||
@@ -37,52 +36,127 @@ struct SettingsView: View {
|
||||
@State private var directoryHidden = false
|
||||
@State private var profileMsg = ""
|
||||
|
||||
// Per-setting sync flags (account-wide; refreshed from the server on pull).
|
||||
@State private var syncFlags: [String: Bool] = SettingsSync.flags()
|
||||
|
||||
// Canonical default colours (single source for the reset buttons).
|
||||
private static let defaultHex: [String: String] = [
|
||||
"primaryColor": "#4285F4", "accentColor": "#EA4335", "todayColor": "#4285F4",
|
||||
"textColor": "#FFFFFF", "backgroundColor": "#000000", "lineColor": "#3A3A52",
|
||||
"monthDividerColor": "#7090C0", "monthLabelColor": "#7090C0",
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
globalSyncSection
|
||||
profilSection
|
||||
privatsphaereSection
|
||||
termindauerSection
|
||||
benachrichtigungenSection
|
||||
geteilterKalenderSection
|
||||
liquidGlassSection
|
||||
cacheSection
|
||||
spracheSection
|
||||
farbenSection
|
||||
schriftSection
|
||||
linienSection
|
||||
termineSection
|
||||
ansichtSection
|
||||
stundenSection
|
||||
farbenSection
|
||||
cacheSection
|
||||
geraetSection
|
||||
}
|
||||
.navigationTitle(L10n.t("settings.title", appLang))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
}
|
||||
// Reflect the latest server values when opening the screen.
|
||||
.task { await SettingsSync.pull(api: api) }
|
||||
// Reflect the latest server values (and flag map) when opening the screen.
|
||||
.task { await SettingsSync.pull(api: api); syncFlags = SettingsSync.flags() }
|
||||
.task { await loadProfile() }
|
||||
// Appearance changes update widgets live; synced values are also pushed
|
||||
// to the server (debounced). `push` itself decides what actually gets
|
||||
// sent based on the sync toggle, so every change can simply call it.
|
||||
// Value edits update widgets live; a synced value is also pushed (debounced).
|
||||
// `push` decides what actually gets sent based on each key's flag.
|
||||
.onChange(of: primaryHex) { _, _ in WidgetStore.republishAppearanceOnly(); SettingsSync.push(api: api) }
|
||||
.onChange(of: accentHex) { _, _ in WidgetStore.republishAppearanceOnly(); SettingsSync.push(api: api) }
|
||||
.onChange(of: todayHex) { _, _ in WidgetStore.republishAppearanceOnly(); SettingsSync.push(api: api) }
|
||||
.onChange(of: textHex) { _, _ in WidgetStore.republishAppearanceOnly(); SettingsSync.push(api: api) }
|
||||
.onChange(of: bgHex) { _, _ in WidgetStore.republishAppearanceOnly(); SettingsSync.push(api: api) }
|
||||
.onChange(of: lineHex) { _, _ in WidgetStore.republishAppearanceOnly(); SettingsSync.push(api: api) }
|
||||
.onChange(of: dividerHex) { _, _ in SettingsSync.push(api: api) }
|
||||
.onChange(of: labelHex) { _, _ in SettingsSync.push(api: api) }
|
||||
.onChange(of: textContrast) { _, _ in SettingsSync.push(api: api) }
|
||||
.onChange(of: lineContrast) { _, _ in SettingsSync.push(api: api) }
|
||||
.onChange(of: dividerHex) { _, _ in WidgetStore.republishAppearanceOnly(); SettingsSync.push(api: api) }
|
||||
.onChange(of: labelHex) { _, _ in WidgetStore.republishAppearanceOnly(); SettingsSync.push(api: api) }
|
||||
.onChange(of: hourHeight) { _, _ in SettingsSync.push(api: api) }
|
||||
.onChange(of: defaultView) { _, _ in SettingsSync.push(api: api) }
|
||||
.onChange(of: weekStartDay) { _, _ in SettingsSync.push(api: api) }
|
||||
.onChange(of: dimPastEvents) { _, _ in SettingsSync.push(api: api) }
|
||||
.onChange(of: monthViewPaged){ _, _ in SettingsSync.push(api: api) }
|
||||
.onChange(of: cacheMonths) { _, _ in SettingsSync.push(api: api) }
|
||||
.onChange(of: defaultEventDurationMinutes) { _, _ in SettingsSync.push(api: api) }
|
||||
.onChange(of: appLang) { _, _ in WidgetStore.republishAppearanceOnly() }
|
||||
// Enabling sync adopts the server's appearance (server wins).
|
||||
.onChange(of: settingsSync) { _, on in if on { Task { await SettingsSync.pull(api: api) } } }
|
||||
}
|
||||
|
||||
// MARK: – Profil
|
||||
// MARK: – Reusable row builders
|
||||
|
||||
/// Leading per-row sync toggle: linked (accent) = synced across devices.
|
||||
@ViewBuilder
|
||||
private func syncIcon(_ key: String) -> some View {
|
||||
let on = syncFlags[key] == true
|
||||
Button {
|
||||
SettingsSync.setSynced(key, !on, api: api)
|
||||
syncFlags = SettingsSync.flags()
|
||||
} label: {
|
||||
Image(systemName: on ? "link.circle.fill" : "link.circle")
|
||||
.imageScale(.large)
|
||||
.foregroundStyle(on ? Color.accentColor : Color.secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(L10n.t("settings.sync_this", appLang))
|
||||
}
|
||||
|
||||
private func colorBinding(_ hex: Binding<String>) -> Binding<Color> {
|
||||
Binding(get: { Color(hex: hex.wrappedValue) }, set: { hex.wrappedValue = $0.toHex() })
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func colorRow(_ syncKey: String, _ defaultsKey: String, _ label: String, _ hex: Binding<String>) -> some View {
|
||||
HStack(spacing: 12) {
|
||||
syncIcon(syncKey)
|
||||
Text(label)
|
||||
Spacer()
|
||||
ColorPicker("", selection: colorBinding(hex), supportsOpacity: false)
|
||||
.labelsHidden()
|
||||
Text(hex.wrappedValue.uppercased())
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(width: 64, alignment: .trailing)
|
||||
Button {
|
||||
hex.wrappedValue = Self.defaultHex[defaultsKey] ?? "#000000"
|
||||
} label: {
|
||||
Image(systemName: "arrow.uturn.backward")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(.secondary)
|
||||
.accessibilityLabel(L10n.t("settings.reset", appLang))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Global sync
|
||||
|
||||
var globalSyncSection: some View {
|
||||
Section {
|
||||
Toggle(isOn: allSyncedBinding) {
|
||||
Label {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(L10n.t("settings.sync_all", appLang))
|
||||
Text(L10n.t("settings.sync_all.desc", appLang))
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
} icon: {
|
||||
Image(systemName: "arrow.triangle.2.circlepath").foregroundStyle(.teal)
|
||||
}
|
||||
}
|
||||
.tint(Color.accentColor)
|
||||
}
|
||||
}
|
||||
|
||||
private var allSyncedBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { SettingsSync.syncableKeys.allSatisfy { syncFlags[$0] == true } },
|
||||
set: { on in SettingsSync.setAllSynced(on, api: api); syncFlags = SettingsSync.flags() }
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: – Profil (account identity — not synced)
|
||||
|
||||
var profilSection: some View {
|
||||
Section(L10n.t("settings.nav.profile", appLang)) {
|
||||
@@ -119,28 +193,7 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Benachrichtigungen
|
||||
|
||||
var benachrichtigungenSection: some View {
|
||||
Section {
|
||||
Picker(ReminderOptions.defaultTitle(appLang), selection: $defaultReminderMinutes) {
|
||||
Text(ReminderOptions.off(appLang)).tag(-1)
|
||||
ForEach(ReminderOptions.all, id: \.self) { m in
|
||||
Text(ReminderOptions.label(m, appLang)).tag(m)
|
||||
}
|
||||
}
|
||||
.onChange(of: defaultReminderMinutes) { _, _ in
|
||||
SettingsSync.push(api: api)
|
||||
NotificationCenter.default.post(name: .rescheduleReminders, object: nil)
|
||||
}
|
||||
} header: {
|
||||
Text(ReminderOptions.sectionTitle(appLang))
|
||||
} footer: {
|
||||
Text(ReminderOptions.defaultFooter(appLang)).font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Privatsphäre
|
||||
// MARK: – Privatsphäre (account — not synced)
|
||||
|
||||
var privatsphaereSection: some View {
|
||||
Section {
|
||||
@@ -158,22 +211,7 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Standardtermindauer
|
||||
|
||||
var termindauerSection: some View {
|
||||
Section {
|
||||
Picker(L10n.t("settings.default_duration", appLang), selection: $defaultEventDurationMinutes) {
|
||||
ForEach([15, 30, 45, 60, 90, 120, 240], id: \.self) { m in
|
||||
Text(ReminderOptions.durationLabel(m, appLang)).tag(m)
|
||||
}
|
||||
}
|
||||
.onChange(of: defaultEventDurationMinutes) { _, _ in SettingsSync.push(api: api) }
|
||||
} header: {
|
||||
Text(L10n.t("settings.calview", appLang))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Geteilter Kalender
|
||||
// MARK: – Geteilter Kalender (account — not synced)
|
||||
|
||||
var geteilterKalenderSection: some View {
|
||||
Section {
|
||||
@@ -193,6 +231,170 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Termine (synced)
|
||||
|
||||
var termineSection: some View {
|
||||
Section(L10n.t("settings.calview", appLang)) {
|
||||
HStack(spacing: 12) {
|
||||
syncIcon("default_event_duration_minutes")
|
||||
Text(L10n.t("settings.default_duration", appLang))
|
||||
Spacer()
|
||||
Picker("", selection: $defaultEventDurationMinutes) {
|
||||
ForEach([15, 30, 45, 60, 90, 120, 240], id: \.self) { m in
|
||||
Text(ReminderOptions.durationLabel(m, appLang)).tag(m)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu).labelsHidden()
|
||||
}
|
||||
HStack(spacing: 12) {
|
||||
syncIcon("default_reminder_minutes")
|
||||
Text(ReminderOptions.defaultTitle(appLang))
|
||||
Spacer()
|
||||
Picker("", selection: $defaultReminderMinutes) {
|
||||
Text(ReminderOptions.off(appLang)).tag(-1)
|
||||
ForEach(ReminderOptions.all, id: \.self) { m in
|
||||
Text(ReminderOptions.label(m, appLang)).tag(m)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu).labelsHidden()
|
||||
.onChange(of: defaultReminderMinutes) { _, _ in
|
||||
SettingsSync.push(api: api)
|
||||
NotificationCenter.default.post(name: .rescheduleReminders, object: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Ansicht (synced)
|
||||
|
||||
var ansichtSection: some View {
|
||||
Section(L10n.t("settings.appearance", appLang)) {
|
||||
HStack(spacing: 12) {
|
||||
syncIcon("default_view")
|
||||
Text(L10n.t("settings.defaultview", appLang))
|
||||
Spacer()
|
||||
Picker("", selection: $defaultView) {
|
||||
Text(L10n.t("view.month", appLang)).tag("month")
|
||||
Text(L10n.t("view.week", appLang)).tag("week")
|
||||
Text(L10n.t("view.day", appLang)).tag("day")
|
||||
Text(L10n.t("view.quarter", appLang)).tag("quarter")
|
||||
Text(L10n.t("view.agenda", appLang)).tag("agenda")
|
||||
}
|
||||
.pickerStyle(.menu).labelsHidden()
|
||||
}
|
||||
HStack(spacing: 12) {
|
||||
syncIcon("week_start_day")
|
||||
Text(L10n.t("settings.firstweekday", appLang))
|
||||
Spacer()
|
||||
Picker("", selection: $weekStartDay) {
|
||||
Text(L10n.t("settings.monday", appLang)).tag("monday")
|
||||
Text(L10n.t("settings.sunday", appLang)).tag("sunday")
|
||||
}
|
||||
.pickerStyle(.menu).labelsHidden()
|
||||
}
|
||||
HStack(spacing: 12) {
|
||||
syncIcon("dim_past_events")
|
||||
Toggle(L10n.t("settings.dimpast", appLang), isOn: $dimPastEvents).tint(Color.accentColor)
|
||||
}
|
||||
HStack(spacing: 12) {
|
||||
syncIcon("month_view_paged")
|
||||
Toggle(L10n.t("settings.month_paged", appLang), isOn: $monthViewPaged).tint(Color.accentColor)
|
||||
}
|
||||
HStack(spacing: 12) {
|
||||
syncIcon("hour_height")
|
||||
Text(L10n.t("settings.hourheight", appLang))
|
||||
Spacer()
|
||||
Picker("", selection: $hourHeight) {
|
||||
Text(L10n.t("settings.hourheight.compact", appLang)).tag(28)
|
||||
Text(L10n.t("settings.hourheight.normal", appLang)).tag(44)
|
||||
Text(L10n.t("settings.hourheight.comfort", appLang)).tag(60)
|
||||
Text(L10n.t("settings.hourheight.large", appLang)).tag(80)
|
||||
}
|
||||
.pickerStyle(.menu).labelsHidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Farben (synced)
|
||||
|
||||
var farbenSection: some View {
|
||||
Section(L10n.t("settings.colors", appLang)) {
|
||||
colorRow("primary_color", "primaryColor", L10n.t("settings.color.primary", appLang), $primaryHex)
|
||||
colorRow("accent_color", "accentColor", L10n.t("settings.color.accent", appLang), $accentHex)
|
||||
colorRow("today_color", "todayColor", L10n.t("settings.color.today", appLang), $todayHex)
|
||||
colorRow("text_color", "textColor", L10n.t("settings.color.text", appLang), $textHex)
|
||||
colorRow("bg_color", "backgroundColor", L10n.t("settings.color.background", appLang), $bgHex)
|
||||
colorRow("line_color", "lineColor", L10n.t("settings.color.line", appLang), $lineHex)
|
||||
colorRow("month_divider_color", "monthDividerColor", L10n.t("settings.color.divider", appLang), $dividerHex)
|
||||
colorRow("month_label_color", "monthLabelColor", L10n.t("settings.color.label", appLang), $labelHex)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Cache (synced)
|
||||
|
||||
var cacheSection: some View {
|
||||
Section {
|
||||
HStack(spacing: 12) {
|
||||
syncIcon("cache_months")
|
||||
Text(L10n.t("settings.cache.range", appLang))
|
||||
Spacer()
|
||||
Picker("", selection: $cacheMonths) {
|
||||
Text(L10n.t("settings.cache.1m", appLang)).tag(1)
|
||||
Text(L10n.t("settings.cache.3m", appLang)).tag(3)
|
||||
Text(L10n.t("settings.cache.6m", appLang)).tag(6)
|
||||
Text(L10n.t("settings.cache.1y", appLang)).tag(12)
|
||||
}
|
||||
.pickerStyle(.menu).labelsHidden()
|
||||
}
|
||||
} header: {
|
||||
Text(L10n.t("settings.cache.header", appLang))
|
||||
} footer: {
|
||||
Text(L10n.t("settings.cache.footer", appLang)).font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Gerät (device-local: language, contrast, liquid glass)
|
||||
|
||||
var geraetSection: some View {
|
||||
Section {
|
||||
Picker(L10n.t("settings.language", appLang), selection: $appLang) {
|
||||
Text(L10n.t("lang.system", appLang)).tag("system")
|
||||
Text(L10n.t("lang.german", appLang)).tag("de")
|
||||
Text(L10n.t("lang.english", appLang)).tag("en")
|
||||
}
|
||||
Picker(L10n.t("settings.textcontrast", appLang), selection: $textContrast) {
|
||||
Text(L10n.t("settings.contrast.dark", appLang)).tag(1)
|
||||
Text(L10n.t("settings.contrast.medium", appLang)).tag(2)
|
||||
Text(L10n.t("settings.contrast.bright", appLang)).tag(3)
|
||||
Text(L10n.t("settings.contrast.max", appLang)).tag(4)
|
||||
}
|
||||
Picker(L10n.t("settings.linecontrast", appLang), selection: $lineContrast) {
|
||||
Text(L10n.t("settings.linecontrast.barely", appLang)).tag(1)
|
||||
Text(L10n.t("settings.linecontrast.subtle", appLang)).tag(2)
|
||||
Text(L10n.t("settings.linecontrast.normal", appLang)).tag(3)
|
||||
Text(L10n.t("settings.linecontrast.strong", appLang)).tag(4)
|
||||
}
|
||||
Toggle(isOn: $liquidGlass) {
|
||||
Label {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(L10n.t("settings.liquidglass", appLang))
|
||||
Text(L10n.t("settings.liquidglass.desc", appLang))
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
} icon: {
|
||||
Image(systemName: "sparkles").foregroundStyle(.blue)
|
||||
}
|
||||
}
|
||||
.tint(Color.accentColor)
|
||||
} header: {
|
||||
Text(L10n.t("settings.device", appLang))
|
||||
} footer: {
|
||||
Text(L10n.t("settings.device.footer", appLang)).font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Profile load / save
|
||||
|
||||
private func loadProfile() async {
|
||||
if let p = try? await api.getProfile() {
|
||||
displayName = p.displayName ?? p.username
|
||||
@@ -223,252 +425,4 @@ struct SettingsView: View {
|
||||
profileMsg = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Liquid Glass
|
||||
|
||||
var liquidGlassSection: some View {
|
||||
Section {
|
||||
Toggle(isOn: $liquidGlass) {
|
||||
Label {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(L10n.t("settings.liquidglass", appLang))
|
||||
Text(L10n.t("settings.liquidglass.desc", appLang))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} icon: {
|
||||
Image(systemName: "sparkles")
|
||||
.foregroundStyle(.blue)
|
||||
}
|
||||
}
|
||||
.tint(Color.accentColor)
|
||||
|
||||
Toggle(isOn: $settingsSync) {
|
||||
Label {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(L10n.t("settings.sync", appLang))
|
||||
Text(L10n.t("settings.sync.desc", appLang))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} icon: {
|
||||
Image(systemName: "arrow.triangle.2.circlepath")
|
||||
.foregroundStyle(.teal)
|
||||
}
|
||||
}
|
||||
.tint(Color.accentColor)
|
||||
} header: {
|
||||
Text(L10n.t("settings.appdesign", appLang))
|
||||
} footer: {
|
||||
Text(L10n.t("settings.sync.footer", appLang))
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Cache
|
||||
|
||||
var cacheSection: some View {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Label {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(L10n.t("settings.cache.title", appLang))
|
||||
Text(L10n.t("settings.cache.desc", appLang))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} icon: {
|
||||
Image(systemName: "arrow.down.circle")
|
||||
.foregroundStyle(.green)
|
||||
}
|
||||
|
||||
Picker(L10n.t("settings.cache.range", appLang), selection: $cacheMonths) {
|
||||
Text(L10n.t("settings.cache.1m", appLang)).tag(1)
|
||||
Text(L10n.t("settings.cache.3m", appLang)).tag(3)
|
||||
Text(L10n.t("settings.cache.6m", appLang)).tag(6)
|
||||
Text(L10n.t("settings.cache.1y", appLang)).tag(12)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
} header: {
|
||||
Text(L10n.t("settings.cache.header", appLang))
|
||||
} footer: {
|
||||
Text(L10n.t("settings.cache.footer", appLang))
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Sprache
|
||||
|
||||
var spracheSection: some View {
|
||||
Section(L10n.t("settings.language", appLang)) {
|
||||
Picker(L10n.t("settings.language", appLang), selection: $appLang) {
|
||||
Text(L10n.t("lang.system", appLang)).tag("system")
|
||||
Text(L10n.t("lang.german", appLang)).tag("de")
|
||||
Text(L10n.t("lang.english", appLang)).tag("en")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Farben
|
||||
|
||||
var farbenSection: some View {
|
||||
Section(L10n.t("settings.colors", appLang)) {
|
||||
ColorPickerRow(label: L10n.t("settings.color.primary", appLang), hex: $primaryHex)
|
||||
ColorPickerRow(label: L10n.t("settings.color.accent", appLang), hex: $accentHex)
|
||||
ColorPickerRow(label: L10n.t("settings.color.today", appLang), hex: $todayHex)
|
||||
ColorPickerRow(label: L10n.t("settings.color.text", appLang), hex: $textHex)
|
||||
ColorPickerRow(label: L10n.t("settings.color.background", appLang), hex: $bgHex)
|
||||
ColorPickerRow(label: L10n.t("settings.color.line", appLang), hex: $lineHex)
|
||||
ColorPickerRow(label: L10n.t("settings.color.divider", appLang), hex: $dividerHex)
|
||||
ColorPickerRow(label: L10n.t("settings.color.label", appLang), hex: $labelHex)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Schriftkontrast
|
||||
|
||||
var schriftSection: some View {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(L10n.t("settings.textcontrast", appLang))
|
||||
.font(.headline)
|
||||
Text(L10n.t("settings.textcontrast.desc", appLang))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
ContrastSelector(
|
||||
value: $textContrast,
|
||||
options: [
|
||||
(1, L10n.t("settings.contrast.dark", appLang)),
|
||||
(2, L10n.t("settings.contrast.medium", appLang)),
|
||||
(3, L10n.t("settings.contrast.bright", appLang)),
|
||||
(4, L10n.t("settings.contrast.max", appLang))
|
||||
]
|
||||
)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Linienkontrast
|
||||
|
||||
var linienSection: some View {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(L10n.t("settings.linecontrast", appLang))
|
||||
.font(.headline)
|
||||
Text(L10n.t("settings.linecontrast.desc", appLang))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
ContrastSelector(
|
||||
value: $lineContrast,
|
||||
options: [
|
||||
(1, L10n.t("settings.linecontrast.barely", appLang)),
|
||||
(2, L10n.t("settings.linecontrast.subtle", appLang)),
|
||||
(3, L10n.t("settings.linecontrast.normal", appLang)),
|
||||
(4, L10n.t("settings.linecontrast.strong", appLang))
|
||||
]
|
||||
)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Ansicht
|
||||
|
||||
var ansichtSection: some View {
|
||||
Section(L10n.t("settings.calview", appLang)) {
|
||||
Picker(L10n.t("settings.defaultview", appLang), selection: $defaultView) {
|
||||
Text(L10n.t("view.month", appLang)).tag("month")
|
||||
Text(L10n.t("view.week", appLang)).tag("week")
|
||||
Text(L10n.t("view.day", appLang)).tag("day")
|
||||
Text(L10n.t("view.quarter", appLang)).tag("quarter")
|
||||
Text(L10n.t("view.agenda", appLang)).tag("agenda")
|
||||
}
|
||||
Picker(L10n.t("settings.firstweekday", appLang), selection: $weekStartDay) {
|
||||
Text(L10n.t("settings.monday", appLang)).tag("monday")
|
||||
Text(L10n.t("settings.sunday", appLang)).tag("sunday")
|
||||
}
|
||||
Toggle(L10n.t("settings.dimpast", appLang), isOn: $dimPastEvents)
|
||||
.tint(Color.accentColor)
|
||||
Toggle(L10n.t("settings.month_paged", appLang), isOn: $monthViewPaged)
|
||||
.tint(Color.accentColor)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Stundenhöhe
|
||||
|
||||
var stundenSection: some View {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(L10n.t("settings.hourheight", appLang))
|
||||
.font(.headline)
|
||||
Text(L10n.t("settings.hourheight.desc", appLang))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
ContrastSelector(
|
||||
value: $hourHeight,
|
||||
options: [
|
||||
(28, L10n.t("settings.hourheight.compact", appLang)),
|
||||
(44, L10n.t("settings.hourheight.normal", appLang)),
|
||||
(60, L10n.t("settings.hourheight.comfort", appLang)),
|
||||
(80, L10n.t("settings.hourheight.large", appLang))
|
||||
]
|
||||
)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: – Reusable Components
|
||||
|
||||
struct ColorPickerRow: View {
|
||||
let label: String
|
||||
@Binding var hex: String
|
||||
|
||||
var color: Binding<Color> {
|
||||
Binding(
|
||||
get: { Color(hex: hex) },
|
||||
set: { hex = $0.toHex() }
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(label)
|
||||
Spacer()
|
||||
ColorPicker("", selection: color, supportsOpacity: false)
|
||||
.labelsHidden()
|
||||
Text(hex.uppercased())
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(width: 68, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContrastSelector<T: Hashable & Equatable>: View {
|
||||
@Binding var value: T
|
||||
let options: [(T, String)]
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(Array(options.enumerated()), id: \.offset) { _, opt in
|
||||
Button {
|
||||
value = opt.0
|
||||
} label: {
|
||||
Text(opt.1)
|
||||
.font(.caption.weight(.medium))
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 8)
|
||||
.background(value == opt.0 ? Color.accentColor : Color(.systemGray5))
|
||||
.foregroundStyle(value == opt.0 ? .white : .primary)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user