import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:pyramid/core/appearance.dart'; import 'package:pyramid/core/read_markers.dart'; import 'package:pyramid/core/theme.dart'; // ── Generic SharedPreferences-backed bool notifier ────────────────────────── class BoolPref extends StateNotifier { final String _key; // Guards against the load race: if set()/toggle() ran before _load's read // lands, the stale persisted value must not overwrite the user's choice. bool _userSet = false; BoolPref(this._key, bool defaultValue) : super(defaultValue) { _load(defaultValue); } Future _load(bool def) async { final p = await SharedPreferences.getInstance(); if (mounted && !_userSet) state = p.getBool(_key) ?? def; } Future set(bool v) async { _userSet = true; state = v; final p = await SharedPreferences.getInstance(); await p.setBool(_key, v); } void toggle() => set(!state); } // ── Generic SharedPreferences-backed String-Set notifier ─────────────────── class StringSetPref extends StateNotifier> { final String _key; late final Future _loaded; StringSetPref(this._key) : super({}) { _loaded = _load(); } Future _load() async { final p = await SharedPreferences.getInstance(); final list = p.getStringList(_key); if (mounted && list != null) state = list.toSet(); } Future add(String v) async { // Read-modify-write: ohne abgeschlossenes Laden würde hier der halbleere // Start-State zurückgeschrieben und persistierte Einträge gingen verloren. await _loaded; if (!mounted || state.contains(v)) return; state = {...state, v}; final p = await SharedPreferences.getInstance(); await p.setStringList(_key, state.toList()); } Future remove(String v) async { await _loaded; if (!mounted || !state.contains(v)) return; state = {...state}..remove(v); final p = await SharedPreferences.getInstance(); await p.setStringList(_key, state.toList()); } } // ── Notification preferences ───────────────────────────────────────────────── final notifDesktopProvider = StateNotifierProvider((ref) => BoolPref('notif_desktop', true)); final notifSoundProvider = StateNotifierProvider((ref) => BoolPref('notif_sound', true)); final notifPreviewProvider = StateNotifierProvider((ref) => BoolPref('notif_preview', true)); final notifMentionOnlyDmProvider = StateNotifierProvider((ref) => BoolPref('notif_mention_dm', false)); // ── Privacy preferences ────────────────────────────────────────────────────── final privacyPresenceProvider = StateNotifierProvider((ref) => BoolPref('privacy_presence', true)); final privacyReadReceiptsProvider = StateNotifierProvider((ref) => BoolPref(readReceiptsPrefKey, true)); final privacyTypingProvider = StateNotifierProvider((ref) => BoolPref('privacy_typing', true)); // ── Voice preferences ──────────────────────────────────────────────────────── // Geschrieben von der Settings-Voice-Sektion, gelesen von den Call-Modulen // (call_signaling, voice_channel). Die historischen voice_*-Keys bleiben // unverändert, damit bestehende Nutzer-Einstellungen erhalten bleiben. class VoicePrefs { VoicePrefs._(); static const _kMic = 'voice_mic_device'; static const _kSpeaker = 'voice_speaker_device'; static const _kCamera = 'voice_camera_device'; static const _kOutputVolume = 'voice_output_volume'; static Future loadOutputVolume() async { final p = await SharedPreferences.getInstance(); return p.getDouble(_kOutputVolume) ?? 1.0; } static Future saveOutputVolume(double v) async { final p = await SharedPreferences.getInstance(); await p.setDouble(_kOutputVolume, v); } /// Gewählte Geräte-IDs; null = kein Pref gesetzt (System-Standard nutzen). static Future<({String? mic, String? speaker, String? camera})> loadDevices() async { final p = await SharedPreferences.getInstance(); return ( mic: p.getString(_kMic), speaker: p.getString(_kSpeaker), camera: p.getString(_kCamera), ); } static Future saveMic(String? deviceId) => _saveDevice(_kMic, deviceId); static Future saveSpeaker(String? deviceId) => _saveDevice(_kSpeaker, deviceId); static Future saveCamera(String? deviceId) => _saveDevice(_kCamera, deviceId); static Future _saveDevice(String key, String? deviceId) async { final p = await SharedPreferences.getInstance(); if (deviceId == null || deviceId.isEmpty) { await p.remove(key); } else { await p.setString(key, deviceId); } } } // ── Theme persistence helpers ──────────────────────────────────────────────── Future saveThemePrefs({ ThemeVariant? variant, int? accentIdx, double? radius, double? motion, double? textScale, bool resetTextScale = false, MessageStyle? messageStyle, }) async { final p = await SharedPreferences.getInstance(); if (variant != null) { await p.setString('theme_variant', variant.name); // Altes Feld mitpflegen (falls jemand auf eine ältere Version zurückgeht). await p.setBool('theme_dark', variant.isDark); } if (accentIdx != null) await p.setInt('theme_accent', accentIdx); if (radius != null) await p.setDouble('theme_radius', radius); if (motion != null) await p.setDouble('theme_motion', motion); if (resetTextScale) { await p.remove('ui_text_scale'); } else if (textScale != null) { await p.setDouble('ui_text_scale', textScale); } if (messageStyle != null) { await p.setString('chat_message_style', messageStyle.name); } } Future< ({ ThemeVariant variant, int accentIdx, double radius, double motion, double? textScale, MessageStyle messageStyle, })> loadThemePrefs() async { final p = await SharedPreferences.getInstance(); // Vor 2026-10-07 gab es nur hell/dunkel ('theme_dark'). Wer dunkel hatte, // bekommt jetzt das neue „Weiche Dunkel“ (Bernds Entscheidung). final storedVariant = p.getString('theme_variant'); final variant = storedVariant != null ? ThemeVariant.fromName(storedVariant) : (p.getBool('theme_dark') ?? true) ? ThemeVariant.soft : ThemeVariant.light; return ( variant: variant, accentIdx: p.getInt('theme_accent') ?? 0, radius: p.getDouble('theme_radius') ?? 12.0, motion: p.getDouble('theme_motion') ?? 1.0, textScale: p.getDouble('ui_text_scale'), messageStyle: MessageStyle.fromName(p.getString('chat_message_style')), ); } // ── Server banner persistence ───────────────────────────────────────────────── // Stored as a single JSON map under 'server_banners': homeserverUrl → mxcUri const _kServerBannersKey = 'server_banners'; Future> loadAllServerBanners() async { final p = await SharedPreferences.getInstance(); final raw = p.getString(_kServerBannersKey); if (raw == null) return {}; try { final decoded = jsonDecode(raw) as Map; return decoded.map((k, v) => MapEntry(k, v as String)); } catch (_) { return {}; } } Future saveServerBanner(String homeserverUrl, String? mxcUri) async { final p = await SharedPreferences.getInstance(); final current = await loadAllServerBanners(); if (mxcUri == null || mxcUri.isEmpty) { current.remove(homeserverUrl); } else { current[homeserverUrl] = mxcUri; } await p.setString(_kServerBannersKey, jsonEncode(current)); }