new favourites feature
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user