import 'dart:async'; import 'dart:developer'; import 'package:collection/collection.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import '../../../../../storage/settings.dart'; import '../../../../../utils/debouncer.dart'; import '../../../../../view/pages/settings/data/default_settings.dart'; class SettingsCubit extends HydratedCubit { static const _debounceTag = 'settings_persist'; bool _emitScheduled = false; Map? _lastEmittedJson; Timer? _silentSave; SettingsCubit() : super(DefaultSettings.get()); Settings val({bool write = false}) { if (write) { // Defer the emit until the synchronous mutation on the returned object // has finished — without this microtask the cubit emits a copy captured // *before* the assignment, so listeners see the old value. if (!_emitScheduled) { _emitScheduled = true; scheduleMicrotask(() { _emitScheduled = false; _emitFreshInstance(); }); } Debouncer.debounce( _debounceTag, const Duration(milliseconds: 500), _emitFreshInstance, ); } return state; } /// Persists in-place mutations without notifying listeners. For high-frequency /// writes nobody renders live (chat drafts per keystroke): a regular write /// would rebuild the app root and every settings watcher on each change. void saveSilently() { _silentSave?.cancel(); _silentSave = Timer(const Duration(milliseconds: 800), flushSilentSave); } /// Writes a pending [saveSilently] right away (e.g. when the app pauses). void flushSilentSave() { if (_silentSave == null) return; _silentSave!.cancel(); _silentSave = null; HydratedBloc.storage.write(storageToken, state.toJson()); } void _emitFreshInstance() { try { final json = state.toJson(); // The debounced emit usually follows the microtask emit with identical // content; skip it instead of rebuilding every listener a second time. if (const DeepCollectionEquality().equals(json, _lastEmittedJson)) { return; } _lastEmittedJson = json; emit(Settings.fromJson(json)); } catch (e) { log('Failed to refresh settings state: $e'); } } Future reset() async { _silentSave?.cancel(); _silentSave = null; _lastEmittedJson = null; emit(DefaultSettings.get()); } // Modules missing from a stale persisted moduleOrder are handled at read // time by AppModule.effectiveModuleOrder (inserted at their default // position) — no healing on hydration needed. @override Future close() { flushSilentSave(); return super.close(); } @override Settings fromJson(Map json) { try { return Settings.fromJson(json); } catch (_) { try { return Settings.fromJson( _mergeSettings(json, DefaultSettings.get().toJson()), ); } catch (_) { return DefaultSettings.get(); } } } @override Map? toJson(Settings state) => state.toJson(); Map _mergeSettings( Map oldMap, Map newMap, ) { final merged = Map.from(newMap); oldMap.forEach((key, value) { if (merged.containsKey(key)) { if (value is Map && merged[key] is Map) { merged[key] = _mergeSettings( value, merged[key] as Map, ); } else { merged[key] = value; } } }); return merged; } }