moodlit-native/Sources/Features/Settings/SettingsView.swift
till c2b79228d0 Settings/Profil auf geteilte ManaAppearanceSection + ManaAboutSection
- MoodlitAppearanceSection ersetzt die Einzel-Wrapper + handgebauten Block.
- ManaAboutSection (moodlit.mana.how, Verein) in ProfileView statt handgebauter
  Version/Build-Section.

Muster identisch zum build-verifizierten herbatrium; lokal nicht gebaut
(Disk-Limit), wird im Xcode-Fleet-Rebuild verifiziert.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 17:36:34 +02:00

178 lines
4.7 KiB
Swift

import ManaCore
import SwiftUI
/// Settings-Tab Brightness, Animation-Speed und Default-Timer.
/// Lokale Settings (Default-Timer-Dauer) leben in App-Group-Defaults;
/// Server-Settings (animationSpeed, brightness) gehen via PATCH an
/// `/api/v1/preferences`.
///
/// Guest-Mode zeigt einen kleinen Hinweis, dass Server-Settings
/// erst nach Login persistieren die lokalen Werte bleiben aber
/// für die Session aktiv.
public struct SettingsView: View {
@Environment(MoodStore.self) private var store
@Environment(AuthClient.self) private var auth
@AppStorage("moodlit.defaultTimerMinutes", store: UserDefaults(suiteName: AppConfig.appGroup))
private var defaultTimerMinutes: Int = 5
@State private var localBrightness: Double = 1.0
@State private var localSpeed: Preferences.AnimationSpeed = .normal
@State private var savingTask: Task<Void, Never>?
private var isSignedIn: Bool {
if case .signedIn = auth.status { return true }
return false
}
public init() {}
public var body: some View {
Form {
MoodlitAppearanceSection()
Section("Player") {
brightnessRow
speedRow
timerRow
}
Section("Smart Home") {
NavigationLink {
HueSettingsView()
} label: {
Label("Philips Hue", systemImage: "lightbulb.2.fill")
}
NavigationLink {
LIFXSettingsView()
} label: {
Label("LIFX", systemImage: "lightbulb.led.wide")
}
#if os(iOS)
NavigationLink {
HomeKitSettingsView()
} label: {
Label("Apple Home", systemImage: "house.fill")
}
#endif
}
if !isSignedIn {
Section {
Label {
Text("Diese Einstellungen werden erst gespeichert, wenn du dich anmeldest.")
.font(.caption)
} icon: {
Image(systemName: "info.circle")
.foregroundStyle(MoodlitTheme.primary)
}
}
.listRowBackground(MoodlitTheme.surface)
}
}
.navigationTitle("Einstellungen")
.task { hydrateFromStore() }
.onChange(of: store.preferences?.brightness) { _, _ in hydrateFromStore() }
.onChange(of: store.preferences?.animationSpeed) { _, _ in hydrateFromStore() }
}
private var brightnessRow: some View {
VStack(alignment: .leading, spacing: 6) {
HStack {
Image(systemName: "sun.max")
.foregroundStyle(MoodlitTheme.mutedForeground)
Text("Helligkeit")
Spacer()
Text("\(Int((localBrightness * 100).rounded()))%")
.font(.callout.monospacedDigit())
.foregroundStyle(MoodlitTheme.mutedForeground)
}
Slider(
value: $localBrightness,
in: 0.20 ... 1.0,
step: 0.05
) {
Text("Helligkeit")
}
.tint(MoodlitTheme.primary)
.onChange(of: localBrightness) { _, newValue in
debouncedSave { _ in
MoodlitAPI.UpdatePreferencesInput(
brightness: Int((newValue * 100).rounded())
)
}
}
}
}
private var speedRow: some View {
HStack {
Image(systemName: "hare")
.foregroundStyle(MoodlitTheme.mutedForeground)
Text("Animations-Tempo")
Spacer()
Picker("", selection: $localSpeed) {
Text("Langsam").tag(Preferences.AnimationSpeed.slow)
Text("Normal").tag(Preferences.AnimationSpeed.normal)
Text("Schnell").tag(Preferences.AnimationSpeed.fast)
}
.pickerStyle(.menu)
.tint(MoodlitTheme.primary)
.labelsHidden()
.onChange(of: localSpeed) { _, newValue in
debouncedSave { _ in
MoodlitAPI.UpdatePreferencesInput(animationSpeed: newValue)
}
}
}
}
private var timerRow: some View {
HStack {
Image(systemName: "timer")
.foregroundStyle(MoodlitTheme.mutedForeground)
Text("Standard-Timer")
Spacer()
Picker("", selection: $defaultTimerMinutes) {
Text("Aus").tag(0)
Text("1 min").tag(1)
Text("5 min").tag(5)
Text("10 min").tag(10)
Text("15 min").tag(15)
Text("30 min").tag(30)
Text("60 min").tag(60)
}
.pickerStyle(.menu)
.tint(MoodlitTheme.primary)
.labelsHidden()
}
}
private func hydrateFromStore() {
if let prefs = store.preferences {
localBrightness = Double(prefs.brightness) / 100.0
localSpeed = prefs.animationSpeed
}
}
/// PATCH /preferences mit 600ms Debounce schützt vor Slider-
/// Spam. Nur wenn eingeloggt; sonst bleiben lokale `@State`-Werte
/// bis zum nächsten App-Start.
private func debouncedSave(
build: @escaping (Preferences.AnimationSpeed) -> MoodlitAPI.UpdatePreferencesInput
) {
guard isSignedIn else { return }
savingTask?.cancel()
savingTask = Task { @MainActor in
try? await Task.sleep(for: .milliseconds(600))
if Task.isCancelled { return }
let input = build(localSpeed)
do {
let updated = try await MoodlitAPI(auth: auth)
.updatePreferences(input)
store.applyPreferences(updated)
} catch {
// Stiller Fail die UI bleibt auf dem User-Wert.
}
}
}
}