look aheead limit defined by user
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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].
|
||||||
|
|||||||
Reference in New Issue
Block a user