import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.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('privacy_read_receipts', 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({ bool? isDark, int? accentIdx, double? radius, double? motion, }) async { final p = await SharedPreferences.getInstance(); if (isDark != null) await p.setBool('theme_dark', 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); } Future<({bool isDark, int accentIdx, double radius, double motion})> loadThemePrefs() async { final p = await SharedPreferences.getInstance(); return ( isDark: p.getBool('theme_dark') ?? true, accentIdx: p.getInt('theme_accent') ?? 0, radius: p.getDouble('theme_radius') ?? 12.0, motion: p.getDouble('theme_motion') ?? 1.0, ); } // ── 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)); }