/// App preferences beyond theme (Look ahead, Overview favourites, Home widgets). library; import 'package:flutter/foundation.dart'; import '../models/app_cloud_prefs.dart'; import '../models/home_built_in_sections.dart'; import '../models/home_filter_widget.dart'; import '../models/overview_favourite.dart'; import '../services/storage_service.dart'; class PreferencesController extends ChangeNotifier { PreferencesController(this._storage) { _lookAheadMaxYears = _storage.lookAheadMaxYears; _overviewFavourites = _storage.loadOverviewFavourites(); _homeFilterWidgets = _storage.loadHomeFilterWidgets(); _homeBuiltInSections = _storage.loadHomeBuiltInSections(); } final StorageService _storage; late int _lookAheadMaxYears; late List _overviewFavourites; late List _homeFilterWidgets; late HomeBuiltInSections _homeBuiltInSections; /// Fired after favourites are persisted (for cloud push debounce). VoidCallback? onFavouritesPersisted; /// Fired after Home widgets are persisted (for cloud push debounce). VoidCallback? onHomeWidgetsPersisted; /// Fired after Home sections / Look ahead are persisted. VoidCallback? onAppPrefsPersisted; /// Years ahead Look ahead can navigate (1–30, default 5). int get lookAheadMaxYears => _lookAheadMaxYears; /// Snapshot used for PocketBase `app_prefs` sync. AppCloudPrefs get cloudPrefs => AppCloudPrefs( homeBuiltInSections: _homeBuiltInSections, lookAheadMaxYears: _lookAheadMaxYears, ); List get overviewFavourites => List.unmodifiable([ for (final fav in _overviewFavourites) if (!fav.deleted) fav, ]); /// Full list including soft-deleted rows (for PocketBase push). List get overviewFavouritesForSync => List.unmodifiable(_overviewFavourites); List get displayedOverviewFavourites => [ for (final fav in _overviewFavourites) if (!fav.deleted && fav.display) fav, ]; bool get hasDisplayedOverviewFavourites => _overviewFavourites.any((f) => !f.deleted && f.display); List get homeFilterWidgets => List.unmodifiable([ for (final w in _homeFilterWidgets) if (!w.deleted) w, ]); /// Full list including soft-deleted rows (for PocketBase push). List get homeFilterWidgetsForSync => List.unmodifiable(_homeFilterWidgets); List get displayedHomeFilterWidgets => [ for (final w in _homeFilterWidgets) if (!w.deleted && w.display) w, ]; bool get hasDisplayedHomeFilterWidgets => _homeFilterWidgets.any((w) => !w.deleted && w.display); HomeBuiltInSections get homeBuiltInSections => _homeBuiltInSections; Future setHomeBuiltInSections( HomeBuiltInSections sections, { bool schedulePush = true, }) async { if (sections.showTransactions == _homeBuiltInSections.showTransactions && sections.showCategorized == _homeBuiltInSections.showCategorized && sections.showRecent == _homeBuiltInSections.showRecent) { return; } _homeBuiltInSections = sections; notifyListeners(); await _storage.saveHomeBuiltInSections(sections); if (schedulePush) onAppPrefsPersisted?.call(); } Future setHomeShowTransactions(bool value) => setHomeBuiltInSections( _homeBuiltInSections.copyWith(showTransactions: value), ); Future setHomeShowCategorized(bool value) => setHomeBuiltInSections( _homeBuiltInSections.copyWith(showCategorized: value), ); Future setHomeShowRecent(bool value) => setHomeBuiltInSections( _homeBuiltInSections.copyWith(showRecent: value), ); Future setLookAheadMaxYears( int years, { bool schedulePush = true, }) async { final next = years.clamp(1, 30); if (next == _lookAheadMaxYears) return; _lookAheadMaxYears = next; notifyListeners(); await _storage.setLookAheadMaxYears(next); if (schedulePush) onAppPrefsPersisted?.call(); } Future replaceCloudPrefs( AppCloudPrefs prefs, { bool notify = true, bool schedulePush = false, }) async { _homeBuiltInSections = prefs.homeBuiltInSections; _lookAheadMaxYears = prefs.lookAheadMaxYears.clamp(1, 30); if (notify) notifyListeners(); await _storage.saveHomeBuiltInSections(_homeBuiltInSections); await _storage.setLookAheadMaxYears(_lookAheadMaxYears); if (schedulePush) onAppPrefsPersisted?.call(); } Future addOverviewFavourite(OverviewFavourite favourite) async { final stamped = favourite.updatedAt == 0 ? favourite.copyWith(touchUpdatedAt: true) : favourite; _overviewFavourites = [..._overviewFavourites, stamped]; notifyListeners(); await _persistFavourites(); } Future updateOverviewFavourite(OverviewFavourite favourite) async { final index = _overviewFavourites.indexWhere((f) => f.id == favourite.id); if (index < 0) return; final next = [..._overviewFavourites]; next[index] = favourite.copyWith(touchUpdatedAt: true); _overviewFavourites = next; notifyListeners(); await _persistFavourites(); } Future removeOverviewFavourite(String id) async { final index = _overviewFavourites.indexWhere((f) => f.id == id); if (index < 0) return; final current = _overviewFavourites[index]; if (current.deleted) return; final next = [..._overviewFavourites]; next[index] = current.copyWith(deleted: true, touchUpdatedAt: true); _overviewFavourites = next; notifyListeners(); await _persistFavourites(); } Future setOverviewFavouriteDisplay(String id, bool display) async { final index = _overviewFavourites.indexWhere((f) => f.id == id); if (index < 0) return; final current = _overviewFavourites[index]; if (current.deleted || current.display == display) return; await updateOverviewFavourite(current.copyWith(display: display)); } Future replaceOverviewFavourites( List favourites, { bool notify = true, bool schedulePush = false, }) async { _overviewFavourites = List.of(favourites); if (notify) notifyListeners(); await _storage.saveOverviewFavourites(_overviewFavourites); if (schedulePush) onFavouritesPersisted?.call(); } Future addHomeFilterWidget(HomeFilterWidget widget) async { final order = _homeFilterWidgets.where((w) => !w.deleted).isEmpty ? 0 : _homeFilterWidgets .where((w) => !w.deleted) .map((w) => w.sortOrder) .reduce((a, b) => a > b ? a : b) + 1; final stamped = (widget.updatedAt == 0 ? widget.copyWith(touchUpdatedAt: true) : widget) .copyWith( sortOrder: widget.sortOrder == 0 ? order : widget.sortOrder, ); _homeFilterWidgets = [..._homeFilterWidgets, stamped]; notifyListeners(); await _persistHomeWidgets(); } Future updateHomeFilterWidget(HomeFilterWidget widget) async { final index = _homeFilterWidgets.indexWhere((w) => w.id == widget.id); if (index < 0) return; final next = [..._homeFilterWidgets]; next[index] = widget.copyWith(touchUpdatedAt: true); next.sort((a, b) => a.sortOrder.compareTo(b.sortOrder)); _homeFilterWidgets = next; notifyListeners(); await _persistHomeWidgets(); } Future removeHomeFilterWidget(String id) async { final index = _homeFilterWidgets.indexWhere((w) => w.id == id); if (index < 0) return; final current = _homeFilterWidgets[index]; if (current.deleted) return; final next = [..._homeFilterWidgets]; next[index] = current.copyWith(deleted: true, touchUpdatedAt: true); _homeFilterWidgets = next; notifyListeners(); await _persistHomeWidgets(); } Future setHomeFilterWidgetDisplay(String id, bool display) async { final index = _homeFilterWidgets.indexWhere((w) => w.id == id); if (index < 0) return; final current = _homeFilterWidgets[index]; if (current.deleted || current.display == display) return; await updateHomeFilterWidget(current.copyWith(display: display)); } Future reorderHomeFilterWidgets(int oldIndex, int newIndex) async { if (oldIndex == newIndex) return; final visible = [ for (final w in _homeFilterWidgets) if (!w.deleted) w, ]; if (oldIndex < 0 || oldIndex >= visible.length) return; final nextVisible = [...visible]; final item = nextVisible.removeAt(oldIndex); final target = newIndex.clamp(0, nextVisible.length); nextVisible.insert(target, item); final orderById = { for (var i = 0; i < nextVisible.length; i++) nextVisible[i].id: i, }; final now = DateTime.now().millisecondsSinceEpoch; _homeFilterWidgets = [ for (final w in _homeFilterWidgets) if (w.deleted) w else w.copyWith(sortOrder: orderById[w.id] ?? w.sortOrder, updatedAt: now), ]..sort((a, b) => a.sortOrder.compareTo(b.sortOrder)); notifyListeners(); await _persistHomeWidgets(); } Future replaceHomeFilterWidgets( List widgets, { bool notify = true, bool schedulePush = false, }) async { final next = List.of(widgets) ..sort((a, b) => a.sortOrder.compareTo(b.sortOrder)); _homeFilterWidgets = next; if (notify) notifyListeners(); await _storage.saveHomeFilterWidgets(_homeFilterWidgets); if (schedulePush) onHomeWidgetsPersisted?.call(); } Future _persistFavourites() async { await _storage.saveOverviewFavourites(_overviewFavourites); onFavouritesPersisted?.call(); } Future _persistHomeWidgets() async { await _storage.saveHomeFilterWidgets(_homeFilterWidgets); onHomeWidgetsPersisted?.call(); } }