look aheead limit defined by user

This commit is contained in:
davmar
2026-09-10 09:31:11 +02:00
parent 200ff72e2e
commit 7437cc90ce
5 changed files with 92 additions and 24 deletions
+4
View File
@@ -9,6 +9,7 @@ import 'package:provider/provider.dart';
import 'providers/auth_provider.dart'; import 'providers/auth_provider.dart';
import 'providers/bank_provider.dart'; import 'providers/bank_provider.dart';
import 'providers/preferences_provider.dart';
import 'providers/sync_provider.dart'; import 'providers/sync_provider.dart';
import 'providers/theme_provider.dart'; import 'providers/theme_provider.dart';
import 'providers/tx_meta_provider.dart'; import 'providers/tx_meta_provider.dart';
@@ -96,6 +97,9 @@ class _MyAppState extends State<MyApp> {
providers: [ providers: [
ChangeNotifierProvider<AuthController>.value(value: widget.auth), ChangeNotifierProvider<AuthController>.value(value: widget.auth),
ChangeNotifierProvider(create: (_) => ThemeController(widget.storage)), ChangeNotifierProvider(create: (_) => ThemeController(widget.storage)),
ChangeNotifierProvider(
create: (_) => PreferencesController(widget.storage),
),
ChangeNotifierProvider<TxMetaController>.value(value: _meta), ChangeNotifierProvider<TxMetaController>.value(value: _meta),
ChangeNotifierProvider<SyncController>.value(value: _sync), ChangeNotifierProvider<SyncController>.value(value: _sync),
ChangeNotifierProvider<BankController>.value(value: _bank), ChangeNotifierProvider<BankController>.value(value: _bank),
+47 -24
View File
@@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
import '../models/bank.dart'; import '../models/bank.dart';
import '../models/transaction_meta.dart'; import '../models/transaction_meta.dart';
import '../providers/bank_provider.dart'; import '../providers/bank_provider.dart';
import '../providers/preferences_provider.dart';
import '../providers/tx_meta_provider.dart'; import '../providers/tx_meta_provider.dart';
import '../screens/transaction_detail_screen.dart'; import '../screens/transaction_detail_screen.dart';
import '../utils/app_theme.dart'; import '../utils/app_theme.dart';
@@ -37,8 +38,6 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
List<RecurringItem>? _cachedItems; List<RecurringItem>? _cachedItems;
int? _itemsCacheKey; int? _itemsCacheKey;
static const _maxYearsAhead = 5;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -51,11 +50,6 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
DateTime get _minMonth => monthStart(_today); 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. /// Inclusive end date for the selected period.
DateTime get _through { DateTime get _through {
if (_period == _PeriodMode.yearly) { if (_period == _PeriodMode.yearly) {
@@ -69,50 +63,67 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
return formatMonthYear(_cursor); 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) { if (_period == _PeriodMode.yearly) {
return _cursor.year > _today.year; return _cursor.year > _today.year;
} }
return monthStart(_cursor).isAfter(_minMonth); return monthStart(_cursor).isAfter(_minMonth);
} }
bool get _canGoForward { bool _canGoForward(int maxYears) {
if (_period == _PeriodMode.yearly) { 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; if (mode == _period) return;
setState(() { setState(() {
_period = mode; _period = mode;
if (mode == _PeriodMode.yearly) { if (mode == _PeriodMode.yearly) {
_cursor = DateTime(_cursor.year); _cursor = DateTime(_cursor.year);
if (_cursor.year < _today.year) _cursor = DateTime(_today.year); if (_cursor.year < _today.year) _cursor = DateTime(_today.year);
if (_cursor.year > _today.year + _maxYearsAhead) { if (_cursor.year > _today.year + maxYears) {
_cursor = DateTime(_today.year + _maxYearsAhead); _cursor = DateTime(_today.year + maxYears);
} }
} else { } else {
final month = DateTime(_cursor.year, _today.month); final month = DateTime(_cursor.year, _today.month);
_cursor = month.isBefore(_minMonth) ? _minMonth : monthStart(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(() { setState(() {
if (_period == _PeriodMode.yearly) { if (_period == _PeriodMode.yearly) {
final next = DateTime(_cursor.year + delta); final next = DateTime(_cursor.year + delta);
if (next.year < _today.year || if (next.year < _today.year || next.year > _today.year + maxYears) {
next.year > _today.year + _maxYearsAhead) {
return; return;
} }
_cursor = next; _cursor = next;
} else { } else {
final next = addMonths(monthStart(_cursor), delta); 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; _cursor = next;
} }
}); });
@@ -131,6 +142,18 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
Widget _buildContent(BuildContext context) { Widget _buildContent(BuildContext context) {
final bank = context.watch<BankController>(); final bank = context.watch<BankController>();
final meta = context.watch<TxMetaController>(); final meta = context.watch<TxMetaController>();
final maxYears =
context.watch<PreferencesController>().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 theme = Theme.of(context);
final colors = theme.colorScheme; final colors = theme.colorScheme;
@@ -202,15 +225,15 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
children: [ children: [
_PeriodToolbar( _PeriodToolbar(
period: _period, period: _period,
onPeriodChanged: _setPeriod, onPeriodChanged: (mode) => _setPeriod(mode, maxYears),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
_PeriodNavigator( _PeriodNavigator(
title: _periodTitle, title: _periodTitle,
canGoBack: _canGoBack, canGoBack: _canGoBack(),
canGoForward: _canGoForward, canGoForward: _canGoForward(maxYears),
onBack: () => _go(-1), onBack: () => _go(-1, maxYears),
onForward: () => _go(1), onForward: () => _go(1, maxYears),
backTooltip: backTooltip:
_period == _PeriodMode.yearly ? 'Previous year' : 'Previous month', _period == _PeriodMode.yearly ? 'Previous year' : 'Previous month',
forwardTooltip: forwardTooltip:
+27
View File
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/bank_provider.dart'; import '../providers/bank_provider.dart';
import '../providers/preferences_provider.dart';
import '../providers/theme_provider.dart'; import '../providers/theme_provider.dart';
import '../utils/connect_bank.dart'; import '../utils/connect_bank.dart';
import '../utils/constants.dart'; import '../utils/constants.dart';
@@ -110,6 +111,7 @@ class SettingsScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final themeController = context.watch<ThemeController>(); final themeController = context.watch<ThemeController>();
final prefs = context.watch<PreferencesController>();
final bank = context.watch<BankController>(); final bank = context.watch<BankController>();
final theme = Theme.of(context); final theme = Theme.of(context);
@@ -150,6 +152,31 @@ class SettingsScreen extends StatelessWidget {
}, },
), ),
const SizedBox(height: 28), 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<int>(
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), Text('Backend', style: theme.textTheme.titleMedium),
const SizedBox(height: 8), const SizedBox(height: 8),
Card( Card(
+11
View File
@@ -122,6 +122,17 @@ class StorageService {
return _prefs.setString(StorageKeys.themeMode, value); 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<void> setLookAheadMaxYears(int years) {
return _prefs.setInt(StorageKeys.lookAheadMaxYears, years.clamp(1, 30));
}
bool get useRemoteData => _prefs.getBool(StorageKeys.useRemoteData) ?? false; bool get useRemoteData => _prefs.getBool(StorageKeys.useRemoteData) ?? false;
Future<void> setUseRemoteData(bool value) { Future<void> setUseRemoteData(bool value) {
+3
View File
@@ -48,6 +48,9 @@ abstract final class StorageKeys {
/// Serialized PocketBase auth token + user record (separate from tx data). /// Serialized PocketBase auth token + user record (separate from tx data).
static const pocketBaseAuth = 'pb_auth_v1'; 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]. /// Named routes registered on [MaterialApp].