- 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>
178 lines
4.7 KiB
Swift
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.
|
|
}
|
|
}
|
|
}
|
|
}
|