Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22a535a6fd | ||
|
|
bd3bc5357e |
@@ -2,6 +2,7 @@
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -46,6 +47,8 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
email: _emailController.text,
|
||||
password: _passwordController.text,
|
||||
);
|
||||
// Lets iOS/Android password managers offer to save / confirm the login.
|
||||
TextInput.finishAutofillContext();
|
||||
if (!mounted) return;
|
||||
if (!widget.asRoot) {
|
||||
Navigator.of(context).pop(true);
|
||||
@@ -96,7 +99,8 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Form(
|
||||
AutofillGroup(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -106,6 +110,10 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
textInputAction: TextInputAction.next,
|
||||
autofocus: true,
|
||||
autofillHints: const [
|
||||
AutofillHints.email,
|
||||
AutofillHints.username,
|
||||
],
|
||||
validator: (value) {
|
||||
final text = value?.trim() ?? '';
|
||||
if (text.isEmpty) return 'Enter your email';
|
||||
@@ -118,6 +126,9 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
controller: _passwordController,
|
||||
obscureText: _obscurePassword,
|
||||
textInputAction: TextInputAction.done,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
onFieldSubmitted: (_) => _submit(),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
@@ -160,6 +171,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -3,6 +3,7 @@ library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -33,6 +34,18 @@ class _ItemsSnapshot {
|
||||
final List<RecurringItem> items;
|
||||
}
|
||||
|
||||
class _BalancePoint {
|
||||
const _BalancePoint({
|
||||
required this.key,
|
||||
required this.label,
|
||||
required this.balance,
|
||||
});
|
||||
|
||||
final DateTime key;
|
||||
final String label;
|
||||
final double balance;
|
||||
}
|
||||
|
||||
class LookAheadScreen extends StatefulWidget {
|
||||
const LookAheadScreen({super.key});
|
||||
|
||||
@@ -48,6 +61,7 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
|
||||
_ItemsSnapshot? _snapshot;
|
||||
int? _loadingKey;
|
||||
int _computeGen = 0;
|
||||
var _showChart = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -63,14 +77,15 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
|
||||
|
||||
/// Inclusive end date for the selected period.
|
||||
DateTime get _through {
|
||||
if (_period == _PeriodMode.yearly) {
|
||||
// Chart view (and yearly list) always project through year-end.
|
||||
if (_showChart || _period == _PeriodMode.yearly) {
|
||||
return DateTime(_cursor.year, 12, 31);
|
||||
}
|
||||
return DateTime(_cursor.year, _cursor.month + 1, 0);
|
||||
}
|
||||
|
||||
String get _periodTitle {
|
||||
if (_period == _PeriodMode.yearly) return '${_cursor.year}';
|
||||
if (_showChart || _period == _PeriodMode.yearly) return '${_cursor.year}';
|
||||
return formatMonthYear(_cursor);
|
||||
}
|
||||
|
||||
@@ -79,28 +94,32 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
|
||||
return DateTime(t.year + maxYears, t.month);
|
||||
}
|
||||
|
||||
int _maxYear(int maxYears) => _today.year + maxYears;
|
||||
|
||||
bool get _navByYear => _showChart || _period == _PeriodMode.yearly;
|
||||
|
||||
bool _canGoBack() {
|
||||
if (_period == _PeriodMode.yearly) {
|
||||
return _cursor.year > _today.year;
|
||||
}
|
||||
if (_navByYear) return _cursor.year > _today.year;
|
||||
return monthStart(_cursor).isAfter(_minMonth);
|
||||
}
|
||||
|
||||
bool _canGoForward(int maxYears) {
|
||||
if (_period == _PeriodMode.yearly) {
|
||||
return _cursor.year < _today.year + maxYears;
|
||||
}
|
||||
if (_navByYear) return _cursor.year < _maxYear(maxYears);
|
||||
return monthStart(_cursor).isBefore(_maxMonthFor(maxYears));
|
||||
}
|
||||
|
||||
void _clampCursor(int maxYears) {
|
||||
if (_period == _PeriodMode.yearly) {
|
||||
if (_cursor.year > _today.year + maxYears) {
|
||||
_cursor = DateTime(_today.year + maxYears);
|
||||
if (_navByYear) {
|
||||
if (_cursor.year > _maxYear(maxYears)) {
|
||||
_cursor = DateTime(_maxYear(maxYears));
|
||||
}
|
||||
if (_cursor.year < _today.year) {
|
||||
_cursor = DateTime(_today.year);
|
||||
}
|
||||
} else {
|
||||
final maxMonth = _maxMonthFor(maxYears);
|
||||
if (_cursor.isAfter(maxMonth)) _cursor = maxMonth;
|
||||
if (_cursor.isBefore(_minMonth)) _cursor = _minMonth;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,26 +127,22 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
|
||||
if (mode == _period) return;
|
||||
setState(() {
|
||||
_period = mode;
|
||||
if (mode == _PeriodMode.yearly) {
|
||||
if (mode == _PeriodMode.yearly || _showChart) {
|
||||
_cursor = DateTime(_cursor.year);
|
||||
if (_cursor.year < _today.year) _cursor = DateTime(_today.year);
|
||||
if (_cursor.year > _today.year + maxYears) {
|
||||
_cursor = DateTime(_today.year + maxYears);
|
||||
}
|
||||
_clampCursor(maxYears);
|
||||
} else {
|
||||
final month = DateTime(_cursor.year, _today.month);
|
||||
_cursor = month.isBefore(_minMonth) ? _minMonth : monthStart(month);
|
||||
final maxMonth = _maxMonthFor(maxYears);
|
||||
if (_cursor.isAfter(maxMonth)) _cursor = maxMonth;
|
||||
_clampCursor(maxYears);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _go(int delta, int maxYears) {
|
||||
setState(() {
|
||||
if (_period == _PeriodMode.yearly) {
|
||||
if (_navByYear) {
|
||||
final next = DateTime(_cursor.year + delta);
|
||||
if (next.year < _today.year || next.year > _today.year + maxYears) {
|
||||
if (next.year < _today.year || next.year > _maxYear(maxYears)) {
|
||||
return;
|
||||
}
|
||||
_cursor = next;
|
||||
@@ -140,9 +155,80 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
void _toggleChart(int maxYears) {
|
||||
setState(() {
|
||||
_showChart = !_showChart;
|
||||
if (_showChart) {
|
||||
_cursor = DateTime(_cursor.year);
|
||||
} else if (_period == _PeriodMode.monthly) {
|
||||
final month = DateTime(_cursor.year, _today.month);
|
||||
_cursor = month.isBefore(_minMonth) ? _minMonth : monthStart(month);
|
||||
}
|
||||
_clampCursor(maxYears);
|
||||
});
|
||||
}
|
||||
|
||||
int _itemsCacheKey(TxMetaController meta, BankController bank) =>
|
||||
Object.hash(meta.storeRevision, identityHashCode(bank.transactions));
|
||||
|
||||
/// Projected account balance at each month-end / year-end.
|
||||
List<_BalancePoint> _balanceSeries({
|
||||
required List<RecurringItem> items,
|
||||
required double currentBalance,
|
||||
required int maxYears,
|
||||
}) {
|
||||
final today = _today;
|
||||
if (_period == _PeriodMode.monthly) {
|
||||
final year = _cursor.year;
|
||||
const months = [
|
||||
'J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D',
|
||||
];
|
||||
final out = <_BalancePoint>[];
|
||||
for (var m = 1; m <= 12; m++) {
|
||||
final end = DateTime(year, m + 1, 0);
|
||||
final balance = !end.isAfter(today)
|
||||
? currentBalance
|
||||
: predictBalance(
|
||||
currentBalance: currentBalance,
|
||||
from: today,
|
||||
throughDate: end,
|
||||
items: items,
|
||||
);
|
||||
out.add(
|
||||
_BalancePoint(
|
||||
key: DateTime(year, m),
|
||||
label: months[m - 1],
|
||||
balance: balance,
|
||||
),
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
final startYear = _today.year;
|
||||
final endYear = _maxYear(maxYears);
|
||||
final out = <_BalancePoint>[];
|
||||
for (var y = startYear; y <= endYear; y++) {
|
||||
final end = DateTime(y, 12, 31);
|
||||
final balance = !end.isAfter(today)
|
||||
? currentBalance
|
||||
: predictBalance(
|
||||
currentBalance: currentBalance,
|
||||
from: today,
|
||||
throughDate: end,
|
||||
items: items,
|
||||
);
|
||||
out.add(
|
||||
_BalancePoint(
|
||||
key: DateTime(y),
|
||||
label: '$y',
|
||||
balance: balance,
|
||||
),
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/// Resolve recurring amounts off the build path, yielding so nav stays live.
|
||||
void _ensureItemsLoaded({
|
||||
required int cacheKey,
|
||||
@@ -261,7 +347,16 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Look ahead'),
|
||||
actions: bankAppBarActions(context: context),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: _showChart ? 'Show list' : 'Show chart',
|
||||
onPressed: () => _toggleChart(maxYears),
|
||||
icon: Icon(
|
||||
_showChart ? Icons.view_list_outlined : Icons.calendar_month,
|
||||
),
|
||||
),
|
||||
...bankAppBarActions(context: context),
|
||||
],
|
||||
),
|
||||
body: const LoadingView(message: 'Calculating look ahead…'),
|
||||
);
|
||||
@@ -303,7 +398,16 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Look ahead'),
|
||||
actions: bankAppBarActions(context: context),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: _showChart ? 'Show list' : 'Show chart',
|
||||
onPressed: () => _toggleChart(maxYears),
|
||||
icon: Icon(
|
||||
_showChart ? Icons.view_list_outlined : Icons.calendar_month,
|
||||
),
|
||||
),
|
||||
...bankAppBarActions(context: context),
|
||||
],
|
||||
),
|
||||
body: ResponsiveBody(
|
||||
child: ListView(
|
||||
@@ -321,10 +425,8 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
|
||||
canGoForward: _canGoForward(maxYears),
|
||||
onBack: () => _go(-1, maxYears),
|
||||
onForward: () => _go(1, maxYears),
|
||||
backTooltip:
|
||||
_period == _PeriodMode.yearly ? 'Previous year' : 'Previous month',
|
||||
forwardTooltip:
|
||||
_period == _PeriodMode.yearly ? 'Next year' : 'Next month',
|
||||
backTooltip: _navByYear ? 'Previous year' : 'Previous month',
|
||||
forwardTooltip: _navByYear ? 'Next year' : 'Next month',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Material(
|
||||
@@ -408,6 +510,28 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_showChart) ...[
|
||||
const SizedBox(height: 16),
|
||||
_LookAheadBalanceChart(
|
||||
points: _balanceSeries(
|
||||
items: items,
|
||||
currentBalance: currentAmount,
|
||||
maxYears: maxYears,
|
||||
),
|
||||
period: _period,
|
||||
currency: currency,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_period == _PeriodMode.yearly
|
||||
? 'Projected account balance by year'
|
||||
: 'Projected account balance by month',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
] else ...[
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
@@ -449,6 +573,11 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_SavingsCard(
|
||||
monthlySavings: monthlyIncome - monthlyExpense,
|
||||
currency: currency,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (items.isEmpty)
|
||||
const EmptyState(
|
||||
icon: Icons.event_repeat_outlined,
|
||||
@@ -485,12 +614,322 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LookAheadBalanceChart extends StatelessWidget {
|
||||
const _LookAheadBalanceChart({
|
||||
required this.points,
|
||||
required this.period,
|
||||
required this.currency,
|
||||
});
|
||||
|
||||
final List<_BalancePoint> points;
|
||||
final _PeriodMode period;
|
||||
final String currency;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colors = theme.colorScheme;
|
||||
if (points.isEmpty) {
|
||||
return Card(
|
||||
color: colors.surfaceContainer,
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
'No projected balance for this period',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
var minY = points.first.balance;
|
||||
var maxY = points.first.balance;
|
||||
for (final p in points) {
|
||||
if (p.balance < minY) minY = p.balance;
|
||||
if (p.balance > maxY) maxY = p.balance;
|
||||
}
|
||||
if (minY == maxY) {
|
||||
minY -= 1;
|
||||
maxY += 1;
|
||||
}
|
||||
final pad = (maxY - minY).abs() * 0.12;
|
||||
final safeMin = minY - pad;
|
||||
final safeMax = maxY + pad;
|
||||
final interval = ((safeMax - safeMin) / 3).abs();
|
||||
final safeInterval = interval <= 0 ? 1.0 : interval;
|
||||
|
||||
final spots = [
|
||||
for (var i = 0; i < points.length; i++)
|
||||
FlSpot(i.toDouble(), points[i].balance),
|
||||
];
|
||||
|
||||
return Card(
|
||||
color: colors.surfaceContainer,
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 14, 14, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'Projected balance',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
minX: 0,
|
||||
maxX: (points.length - 1).toDouble(),
|
||||
minY: safeMin,
|
||||
maxY: safeMax,
|
||||
lineTouchData: LineTouchData(
|
||||
handleBuiltInTouches: true,
|
||||
touchTooltipData: LineTouchTooltipData(
|
||||
getTooltipItems: (touched) {
|
||||
return [
|
||||
for (final spot in touched)
|
||||
if (spot.x.round() >= 0 &&
|
||||
spot.x.round() < points.length)
|
||||
LineTooltipItem(
|
||||
'${points[spot.x.round()].label}\n'
|
||||
'${formatMoney(points[spot.x.round()].balance, currency)}',
|
||||
theme.textTheme.labelMedium!.copyWith(
|
||||
color: colors.onInverseSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 44,
|
||||
interval: safeInterval,
|
||||
getTitlesWidget: (value, meta) {
|
||||
if ((value - safeMax).abs() < 0.01 ||
|
||||
(value - safeMin).abs() < 0.01) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Text(
|
||||
_compactMoney(value),
|
||||
style: theme.textTheme.labelSmall,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 28,
|
||||
interval: 1,
|
||||
getTitlesWidget: (value, meta) {
|
||||
final index = value.round();
|
||||
if (index < 0 || index >= points.length) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (period == _PeriodMode.yearly &&
|
||||
points.length > 8 &&
|
||||
index % 2 == 1) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
points[index].label,
|
||||
style: theme.textTheme.labelSmall,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: safeInterval,
|
||||
getDrawingHorizontalLine: (value) => FlLine(
|
||||
color: colors.outlineVariant.withValues(alpha: 0.35),
|
||||
strokeWidth: 1,
|
||||
dashArray: const [4, 4],
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: spots,
|
||||
isCurved: true,
|
||||
curveSmoothness: 0.18,
|
||||
color: colors.primary,
|
||||
barWidth: 3,
|
||||
isStrokeCapRound: true,
|
||||
dotData: FlDotData(
|
||||
show: true,
|
||||
getDotPainter: (spot, percent, bar, index) {
|
||||
return FlDotCirclePainter(
|
||||
radius: 3.5,
|
||||
color: colors.primary,
|
||||
strokeWidth: 0,
|
||||
);
|
||||
},
|
||||
),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: colors.primary.withValues(alpha: 0.14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
duration: Duration.zero,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SavingsCard extends StatelessWidget {
|
||||
const _SavingsCard({
|
||||
required this.monthlySavings,
|
||||
required this.currency,
|
||||
});
|
||||
|
||||
final double monthlySavings;
|
||||
final String currency;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colors = theme.colorScheme;
|
||||
final yearly = monthlySavings * 12;
|
||||
final positive = monthlySavings >= 0;
|
||||
final tone = positive ? AppTheme.accent : AppTheme.expense;
|
||||
|
||||
return Card(
|
||||
color: colors.surfaceContainer,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 14, 14, 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 42,
|
||||
height: 42,
|
||||
decoration: BoxDecoration(
|
||||
color: tone.withValues(alpha: 0.18),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(Icons.savings_outlined, color: tone, size: 22),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _SavingsMetric(
|
||||
label: 'Monthly savings',
|
||||
amount: formatMoney(monthlySavings, currency),
|
||||
color: tone,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _SavingsMetric(
|
||||
label: 'Yearly savings',
|
||||
amount: formatMoney(yearly, currency),
|
||||
color: tone,
|
||||
alignEnd: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SavingsMetric extends StatelessWidget {
|
||||
const _SavingsMetric({
|
||||
required this.label,
|
||||
required this.amount,
|
||||
required this.color,
|
||||
this.alignEnd = false,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String amount;
|
||||
final Color color;
|
||||
final bool alignEnd;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colors = theme.colorScheme;
|
||||
return Column(
|
||||
crossAxisAlignment:
|
||||
alignEnd ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label.toUpperCase(),
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
letterSpacing: 0.6,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
amount,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _compactMoney(double value) {
|
||||
final abs = value.abs();
|
||||
if (abs >= 1000000) return '${(value / 1000000).toStringAsFixed(1)}M';
|
||||
if (abs >= 1000) return '${(value / 1000).toStringAsFixed(1)}k';
|
||||
if (abs >= 100) return value.toStringAsFixed(0);
|
||||
return value.toStringAsFixed(abs < 10 && abs > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
class _PeriodToolbar extends StatelessWidget {
|
||||
const _PeriodToolbar({
|
||||
required this.period,
|
||||
|
||||
@@ -14,6 +14,10 @@ class AppTextField extends StatelessWidget {
|
||||
this.keyboardType,
|
||||
this.validator,
|
||||
this.autofocus = false,
|
||||
this.autofillHints,
|
||||
this.obscureText = false,
|
||||
this.enableSuggestions,
|
||||
this.autocorrect,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
@@ -24,6 +28,10 @@ class AppTextField extends StatelessWidget {
|
||||
final TextInputType? keyboardType;
|
||||
final String? Function(String?)? validator;
|
||||
final bool autofocus;
|
||||
final Iterable<String>? autofillHints;
|
||||
final bool obscureText;
|
||||
final bool? enableSuggestions;
|
||||
final bool? autocorrect;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -31,8 +39,12 @@ class AppTextField extends StatelessWidget {
|
||||
controller: controller,
|
||||
maxLines: maxLines,
|
||||
autofocus: autofocus,
|
||||
obscureText: obscureText,
|
||||
textInputAction: textInputAction,
|
||||
keyboardType: keyboardType,
|
||||
autofillHints: autofillHints,
|
||||
enableSuggestions: enableSuggestions ?? !obscureText,
|
||||
autocorrect: autocorrect ?? !obscureText,
|
||||
validator: validator,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
|
||||
Reference in New Issue
Block a user