126 lines
3.6 KiB
Dart
126 lines
3.6 KiB
Dart
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<Settings> {
|
|
static const _debounceTag = 'settings_persist';
|
|
bool _emitScheduled = false;
|
|
Map<String, dynamic>? _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<void> 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<void> close() {
|
|
flushSilentSave();
|
|
return super.close();
|
|
}
|
|
|
|
@override
|
|
Settings fromJson(Map<String, dynamic> json) {
|
|
try {
|
|
return Settings.fromJson(json);
|
|
} catch (_) {
|
|
try {
|
|
return Settings.fromJson(
|
|
_mergeSettings(json, DefaultSettings.get().toJson()),
|
|
);
|
|
} catch (_) {
|
|
return DefaultSettings.get();
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Map<String, dynamic>? toJson(Settings state) => state.toJson();
|
|
|
|
Map<String, dynamic> _mergeSettings(
|
|
Map<String, dynamic> oldMap,
|
|
Map<String, dynamic> newMap,
|
|
) {
|
|
final merged = Map<String, dynamic>.from(newMap);
|
|
oldMap.forEach((key, value) {
|
|
if (merged.containsKey(key)) {
|
|
if (value is Map<String, dynamic> &&
|
|
merged[key] is Map<String, dynamic>) {
|
|
merged[key] = _mergeSettings(
|
|
value,
|
|
merged[key] as Map<String, dynamic>,
|
|
);
|
|
} else {
|
|
merged[key] = value;
|
|
}
|
|
}
|
|
});
|
|
return merged;
|
|
}
|
|
}
|