283 lines
9.8 KiB
Dart
283 lines
9.8 KiB
Dart
/// 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<OverviewFavourite> _overviewFavourites;
|
||
late List<HomeFilterWidget> _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<OverviewFavourite> get overviewFavourites =>
|
||
List.unmodifiable([
|
||
for (final fav in _overviewFavourites)
|
||
if (!fav.deleted) fav,
|
||
]);
|
||
|
||
/// Full list including soft-deleted rows (for PocketBase push).
|
||
List<OverviewFavourite> get overviewFavouritesForSync =>
|
||
List.unmodifiable(_overviewFavourites);
|
||
|
||
List<OverviewFavourite> get displayedOverviewFavourites => [
|
||
for (final fav in _overviewFavourites)
|
||
if (!fav.deleted && fav.display) fav,
|
||
];
|
||
|
||
bool get hasDisplayedOverviewFavourites =>
|
||
_overviewFavourites.any((f) => !f.deleted && f.display);
|
||
|
||
List<HomeFilterWidget> get homeFilterWidgets =>
|
||
List.unmodifiable([
|
||
for (final w in _homeFilterWidgets)
|
||
if (!w.deleted) w,
|
||
]);
|
||
|
||
/// Full list including soft-deleted rows (for PocketBase push).
|
||
List<HomeFilterWidget> get homeFilterWidgetsForSync =>
|
||
List.unmodifiable(_homeFilterWidgets);
|
||
|
||
List<HomeFilterWidget> 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<void> 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<void> setHomeShowTransactions(bool value) => setHomeBuiltInSections(
|
||
_homeBuiltInSections.copyWith(showTransactions: value),
|
||
);
|
||
|
||
Future<void> setHomeShowCategorized(bool value) => setHomeBuiltInSections(
|
||
_homeBuiltInSections.copyWith(showCategorized: value),
|
||
);
|
||
|
||
Future<void> setHomeShowRecent(bool value) => setHomeBuiltInSections(
|
||
_homeBuiltInSections.copyWith(showRecent: value),
|
||
);
|
||
|
||
Future<void> 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<void> 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<void> addOverviewFavourite(OverviewFavourite favourite) async {
|
||
final stamped = favourite.updatedAt == 0
|
||
? favourite.copyWith(touchUpdatedAt: true)
|
||
: favourite;
|
||
_overviewFavourites = [..._overviewFavourites, stamped];
|
||
notifyListeners();
|
||
await _persistFavourites();
|
||
}
|
||
|
||
Future<void> 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<void> 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<void> 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<void> replaceOverviewFavourites(
|
||
List<OverviewFavourite> favourites, {
|
||
bool notify = true,
|
||
bool schedulePush = false,
|
||
}) async {
|
||
_overviewFavourites = List.of(favourites);
|
||
if (notify) notifyListeners();
|
||
await _storage.saveOverviewFavourites(_overviewFavourites);
|
||
if (schedulePush) onFavouritesPersisted?.call();
|
||
}
|
||
|
||
Future<void> 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<void> 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<void> 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<void> 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<void> 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 = <String, int>{
|
||
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<void> replaceHomeFilterWidgets(
|
||
List<HomeFilterWidget> 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<void> _persistFavourites() async {
|
||
await _storage.saveOverviewFavourites(_overviewFavourites);
|
||
onFavouritesPersisted?.call();
|
||
}
|
||
|
||
Future<void> _persistHomeWidgets() async {
|
||
await _storage.saveHomeFilterWidgets(_homeFilterWidgets);
|
||
onHomeWidgetsPersisted?.call();
|
||
}
|
||
}
|