new favourites feature

This commit is contained in:
davmar
2026-09-10 16:25:24 +02:00
parent aec89ac41d
commit 83295439d5
22 changed files with 3168 additions and 282 deletions
+149 -1
View File
@@ -1,21 +1,55 @@
/// App preferences beyond theme (Look ahead horizon, etc.).
/// App preferences beyond theme (Look ahead, Overview favourites, Home widgets).
library;
import 'package:flutter/foundation.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();
}
final StorageService _storage;
late int _lookAheadMaxYears;
late List<OverviewFavourite> _overviewFavourites;
late List<HomeFilterWidget> _homeFilterWidgets;
/// Fired after favourites are persisted (for cloud push debounce).
VoidCallback? onFavouritesPersisted;
/// Fired after Home widgets are persisted (for cloud push debounce).
VoidCallback? onHomeWidgetsPersisted;
/// Years ahead Look ahead can navigate (1–30, default 5).
int get lookAheadMaxYears => _lookAheadMaxYears;
List<OverviewFavourite> get overviewFavourites =>
List.unmodifiable(_overviewFavourites);
List<OverviewFavourite> get displayedOverviewFavourites => [
for (final fav in _overviewFavourites)
if (fav.display) fav,
];
bool get hasDisplayedOverviewFavourites =>
_overviewFavourites.any((f) => f.display);
List<HomeFilterWidget> get homeFilterWidgets =>
List.unmodifiable(_homeFilterWidgets);
List<HomeFilterWidget> get displayedHomeFilterWidgets => [
for (final w in _homeFilterWidgets)
if (w.display) w,
];
bool get hasDisplayedHomeFilterWidgets =>
_homeFilterWidgets.any((w) => w.display);
Future<void> setLookAheadMaxYears(int years) async {
final next = years.clamp(1, 30);
if (next == _lookAheadMaxYears) return;
@@ -23,4 +57,118 @@ class PreferencesController extends ChangeNotifier {
notifyListeners();
await _storage.setLookAheadMaxYears(next);
}
Future<void> addOverviewFavourite(OverviewFavourite favourite) async {
_overviewFavourites = [..._overviewFavourites, favourite];
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;
_overviewFavourites = next;
notifyListeners();
await _persistFavourites();
}
Future<void> removeOverviewFavourite(String id) async {
final next = [
for (final fav in _overviewFavourites)
if (fav.id != id) fav,
];
if (next.length == _overviewFavourites.length) return;
_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.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.isEmpty
? 0
: _homeFilterWidgets
.map((w) => w.sortOrder)
.reduce((a, b) => a > b ? a : b) +
1;
_homeFilterWidgets = [
..._homeFilterWidgets,
widget.copyWith(sortOrder: widget.sortOrder == 0 ? order : widget.sortOrder),
];
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;
next.sort((a, b) => a.sortOrder.compareTo(b.sortOrder));
_homeFilterWidgets = next;
notifyListeners();
await _persistHomeWidgets();
}
Future<void> removeHomeFilterWidget(String id) async {
final next = [
for (final w in _homeFilterWidgets)
if (w.id != id) w,
];
if (next.length == _homeFilterWidgets.length) return;
_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.display == display) return;
await updateHomeFilterWidget(current.copyWith(display: display));
}
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();
}
}
+91 -3
View File
@@ -5,9 +5,12 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import '../models/home_filter_widget.dart';
import '../models/overview_favourite.dart';
import '../services/pb_sync_service.dart';
import 'auth_provider.dart';
import 'bank_provider.dart';
import 'preferences_provider.dart';
import 'tx_meta_provider.dart';
/// User-visible sync toast state (watched by [SyncFeedbackListener]).
@@ -30,11 +33,15 @@ class SyncController extends ChangeNotifier {
required AuthController auth,
required TxMetaController meta,
required BankController bank,
required PreferencesController prefs,
}) : _auth = auth,
_meta = meta,
_bank = bank {
_bank = bank,
_prefs = prefs {
_wasLoggedIn = auth.isLoggedIn;
_meta.onPersisted = _schedulePush;
_prefs.onFavouritesPersisted = _schedulePush;
_prefs.onHomeWidgetsPersisted = _schedulePush;
_auth.addListener(_onAuthChanged);
_bank.onAfterSuccessfulLoad = _onBankDataLoaded;
if (_auth.isLoggedIn) {
@@ -45,6 +52,7 @@ class SyncController extends ChangeNotifier {
final AuthController _auth;
final TxMetaController _meta;
final BankController _bank;
final PreferencesController _prefs;
bool _wasLoggedIn = false;
bool _syncing = false;
@@ -260,20 +268,77 @@ class SyncController extends ChangeNotifier {
_setProgress(0.5);
_setStatus('Merging…');
final mergedMeta = mergeTxMetaStores(_meta.exportStore(), remoteMeta);
List<OverviewFavourite> remoteFavourites = const [];
try {
remoteFavourites = await service.pullOverviewFavourites(
onStatus: _setStatus,
);
} catch (error) {
debugPrint(
'SyncController: overview_favourites pull skipped ($error)',
);
}
final mergedFavourites = mergeOverviewFavourites(
_prefs.overviewFavourites,
remoteFavourites,
);
List<HomeFilterWidget> remoteHomeWidgets = const [];
try {
remoteHomeWidgets = await service.pullHomeFilterWidgets(
onStatus: _setStatus,
);
} catch (error) {
debugPrint(
'SyncController: home_filter_widgets pull skipped ($error)',
);
}
final mergedHomeWidgets = mergeHomeFilterWidgets(
_prefs.homeFilterWidgets,
remoteHomeWidgets,
);
// Apply without notifying — one rebuild after both stores are ready.
await _meta.replaceStore(mergedMeta, notify: false);
await _bank.applySyncedTransactions(remoteTxs, notify: false);
await _prefs.replaceOverviewFavourites(
mergedFavourites,
notify: false,
);
await _prefs.replaceHomeFilterWidgets(
mergedHomeWidgets,
notify: false,
);
_meta.notifyListeners();
_bank.notifyListeners();
_prefs.notifyListeners();
_setProgress(0.55);
_setStatus('Pushing…');
final metaSummary = await service.push(
mergedMeta,
progressStart: 0.55,
progressEnd: 0.82,
progressEnd: 0.75,
onProgress: _setProgress,
onStatus: _setStatus,
);
try {
await service.pushOverviewFavourites(
mergedFavourites,
onStatus: _setStatus,
);
} catch (error) {
debugPrint(
'SyncController: overview_favourites push skipped ($error)',
);
}
try {
await service.pushHomeFilterWidgets(
mergedHomeWidgets,
onStatus: _setStatus,
);
} catch (error) {
debugPrint(
'SyncController: home_filter_widgets push skipped ($error)',
);
}
final bankSummary = await service.pushTransactions(
_bank.transactions,
progressStart: 0.82,
@@ -323,10 +388,31 @@ class SyncController extends ChangeNotifier {
final summary = await service.push(
_meta.exportStore(),
progressStart: 0.05,
progressEnd: 1.0,
progressEnd: 0.9,
onProgress: _setProgress,
onStatus: _setStatus,
);
try {
await service.pushOverviewFavourites(
_prefs.overviewFavourites,
onStatus: _setStatus,
);
} catch (error) {
debugPrint(
'SyncController: overview_favourites push skipped ($error)',
);
}
try {
await service.pushHomeFilterWidgets(
_prefs.homeFilterWidgets,
onStatus: _setStatus,
);
} catch (error) {
debugPrint(
'SyncController: home_filter_widgets push skipped ($error)',
);
}
_setProgress(1.0);
_lastSyncedAt = DateTime.now();
_lastError = null;
_lastSyncSummary = summary;
@@ -359,6 +445,8 @@ class SyncController extends ChangeNotifier {
_auth.removeListener(_onAuthChanged);
_bank.onAfterSuccessfulLoad = null;
_meta.onPersisted = null;
_prefs.onFavouritesPersisted = null;
_prefs.onHomeWidgetsPersisted = null;
super.dispose();
}
}