From 7437cc90ce2d35abfe87ba0020690245c06378c5 Mon Sep 17 00:00:00 2001 From: davmar Date: Thu, 10 Sep 2026 09:31:11 +0200 Subject: [PATCH] look aheead limit defined by user --- lib/main.dart | 4 ++ lib/screens/look_ahead_screen.dart | 71 ++++++++++++++++++++---------- lib/screens/settings_screen.dart | 27 ++++++++++++ lib/services/storage_service.dart | 11 +++++ lib/utils/constants.dart | 3 ++ 5 files changed, 92 insertions(+), 24 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index ed4892c..818c8a3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,7 @@ import 'package:provider/provider.dart'; import 'providers/auth_provider.dart'; import 'providers/bank_provider.dart'; +import 'providers/preferences_provider.dart'; import 'providers/sync_provider.dart'; import 'providers/theme_provider.dart'; import 'providers/tx_meta_provider.dart'; @@ -96,6 +97,9 @@ class _MyAppState extends State { providers: [ ChangeNotifierProvider.value(value: widget.auth), ChangeNotifierProvider(create: (_) => ThemeController(widget.storage)), + ChangeNotifierProvider( + create: (_) => PreferencesController(widget.storage), + ), ChangeNotifierProvider.value(value: _meta), ChangeNotifierProvider.value(value: _sync), ChangeNotifierProvider.value(value: _bank), diff --git a/lib/screens/look_ahead_screen.dart b/lib/screens/look_ahead_screen.dart index adfd31e..60450bb 100644 --- a/lib/screens/look_ahead_screen.dart +++ b/lib/screens/look_ahead_screen.dart @@ -7,6 +7,7 @@ import 'package:provider/provider.dart'; import '../models/bank.dart'; import '../models/transaction_meta.dart'; import '../providers/bank_provider.dart'; +import '../providers/preferences_provider.dart'; import '../providers/tx_meta_provider.dart'; import '../screens/transaction_detail_screen.dart'; import '../utils/app_theme.dart'; @@ -37,8 +38,6 @@ class _LookAheadScreenState extends State { List? _cachedItems; int? _itemsCacheKey; - static const _maxYearsAhead = 5; - @override void initState() { super.initState(); @@ -51,11 +50,6 @@ class _LookAheadScreenState extends State { DateTime get _minMonth => monthStart(_today); - DateTime get _maxMonth { - final t = _today; - return DateTime(t.year + _maxYearsAhead, t.month); - } - /// Inclusive end date for the selected period. DateTime get _through { if (_period == _PeriodMode.yearly) { @@ -69,50 +63,67 @@ class _LookAheadScreenState extends State { return formatMonthYear(_cursor); } - bool get _canGoBack { + DateTime _maxMonthFor(int maxYears) { + final t = _today; + return DateTime(t.year + maxYears, t.month); + } + + bool _canGoBack() { if (_period == _PeriodMode.yearly) { return _cursor.year > _today.year; } return monthStart(_cursor).isAfter(_minMonth); } - bool get _canGoForward { + bool _canGoForward(int maxYears) { if (_period == _PeriodMode.yearly) { - return _cursor.year < _today.year + _maxYearsAhead; + return _cursor.year < _today.year + maxYears; } - return monthStart(_cursor).isBefore(_maxMonth); + return monthStart(_cursor).isBefore(_maxMonthFor(maxYears)); } - void _setPeriod(_PeriodMode mode) { + void _clampCursor(int maxYears) { + if (_period == _PeriodMode.yearly) { + if (_cursor.year > _today.year + maxYears) { + _cursor = DateTime(_today.year + maxYears); + } + } else { + final maxMonth = _maxMonthFor(maxYears); + if (_cursor.isAfter(maxMonth)) _cursor = maxMonth; + } + } + + void _setPeriod(_PeriodMode mode, int maxYears) { if (mode == _period) return; setState(() { _period = mode; if (mode == _PeriodMode.yearly) { _cursor = DateTime(_cursor.year); if (_cursor.year < _today.year) _cursor = DateTime(_today.year); - if (_cursor.year > _today.year + _maxYearsAhead) { - _cursor = DateTime(_today.year + _maxYearsAhead); + if (_cursor.year > _today.year + maxYears) { + _cursor = DateTime(_today.year + maxYears); } } else { final month = DateTime(_cursor.year, _today.month); _cursor = month.isBefore(_minMonth) ? _minMonth : monthStart(month); - if (_cursor.isAfter(_maxMonth)) _cursor = _maxMonth; + final maxMonth = _maxMonthFor(maxYears); + if (_cursor.isAfter(maxMonth)) _cursor = maxMonth; } }); } - void _go(int delta) { + void _go(int delta, int maxYears) { setState(() { if (_period == _PeriodMode.yearly) { final next = DateTime(_cursor.year + delta); - if (next.year < _today.year || - next.year > _today.year + _maxYearsAhead) { + if (next.year < _today.year || next.year > _today.year + maxYears) { return; } _cursor = next; } else { final next = addMonths(monthStart(_cursor), delta); - if (next.isBefore(_minMonth) || next.isAfter(_maxMonth)) return; + final maxMonth = _maxMonthFor(maxYears); + if (next.isBefore(_minMonth) || next.isAfter(maxMonth)) return; _cursor = next; } }); @@ -131,6 +142,18 @@ class _LookAheadScreenState extends State { Widget _buildContent(BuildContext context) { final bank = context.watch(); final meta = context.watch(); + final maxYears = + context.watch().lookAheadMaxYears; + final maxMonth = _maxMonthFor(maxYears); + final needsClamp = _period == _PeriodMode.yearly + ? _cursor.year > _today.year + maxYears + : _cursor.isAfter(maxMonth); + if (needsClamp) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + setState(() => _clampCursor(maxYears)); + }); + } final theme = Theme.of(context); final colors = theme.colorScheme; @@ -202,15 +225,15 @@ class _LookAheadScreenState extends State { children: [ _PeriodToolbar( period: _period, - onPeriodChanged: _setPeriod, + onPeriodChanged: (mode) => _setPeriod(mode, maxYears), ), const SizedBox(height: 10), _PeriodNavigator( title: _periodTitle, - canGoBack: _canGoBack, - canGoForward: _canGoForward, - onBack: () => _go(-1), - onForward: () => _go(1), + canGoBack: _canGoBack(), + canGoForward: _canGoForward(maxYears), + onBack: () => _go(-1, maxYears), + onForward: () => _go(1, maxYears), backTooltip: _period == _PeriodMode.yearly ? 'Previous year' : 'Previous month', forwardTooltip: diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 5a225fc..bf4b7fa 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/bank_provider.dart'; +import '../providers/preferences_provider.dart'; import '../providers/theme_provider.dart'; import '../utils/connect_bank.dart'; import '../utils/constants.dart'; @@ -110,6 +111,7 @@ class SettingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { final themeController = context.watch(); + final prefs = context.watch(); final bank = context.watch(); final theme = Theme.of(context); @@ -150,6 +152,31 @@ class SettingsScreen extends StatelessWidget { }, ), const SizedBox(height: 28), + Text('Look ahead', style: theme.textTheme.titleMedium), + const SizedBox(height: 8), + Card( + child: ListTile( + leading: const Icon(Icons.event_repeat_outlined), + title: const Text('Projection horizon'), + subtitle: Text( + 'Navigate up to ${prefs.lookAheadMaxYears} year' + '${prefs.lookAheadMaxYears == 1 ? '' : 's'} ahead', + ), + trailing: DropdownButton( + value: prefs.lookAheadMaxYears, + underline: const SizedBox.shrink(), + items: [ + for (final y in const [1, 2, 3, 5, 7, 10, 15, 20, 30]) + DropdownMenuItem(value: y, child: Text('$y')), + ], + onChanged: (value) { + if (value == null) return; + prefs.setLookAheadMaxYears(value); + }, + ), + ), + ), + const SizedBox(height: 28), Text('Backend', style: theme.textTheme.titleMedium), const SizedBox(height: 8), Card( diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index 24dbd47..efc8e30 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -122,6 +122,17 @@ class StorageService { return _prefs.setString(StorageKeys.themeMode, value); } + /// Look ahead navigation horizon in years (clamped 1–30, default 5). + int get lookAheadMaxYears { + final raw = _prefs.getInt(StorageKeys.lookAheadMaxYears); + if (raw == null) return 5; + return raw.clamp(1, 30); + } + + Future setLookAheadMaxYears(int years) { + return _prefs.setInt(StorageKeys.lookAheadMaxYears, years.clamp(1, 30)); + } + bool get useRemoteData => _prefs.getBool(StorageKeys.useRemoteData) ?? false; Future setUseRemoteData(bool value) { diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index a9781f5..fe7b8b6 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -48,6 +48,9 @@ abstract final class StorageKeys { /// Serialized PocketBase auth token + user record (separate from tx data). static const pocketBaseAuth = 'pb_auth_v1'; + + /// How many years ahead Look ahead can navigate (default 5). + static const lookAheadMaxYears = 'look_ahead_max_years_v1'; } /// Named routes registered on [MaterialApp].