chart improvements, synch working
This commit is contained in:
@@ -790,15 +790,24 @@ class TxMetaController extends ChangeNotifier {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// OR filter: any selected label, and/or unlabeled when uncategorized is selected.
|
/// AND filter: every selected label must be on the tx.
|
||||||
|
///
|
||||||
|
/// Uncategorized alone matches unlabeled txs. Uncategorized combined with
|
||||||
|
/// other labels matches nothing (a tx cannot be both).
|
||||||
bool _matchesLabelFilter(TxMeta meta, Set<String> labelIds) {
|
bool _matchesLabelFilter(TxMeta meta, Set<String> labelIds) {
|
||||||
final wantUncategorized = labelIds.contains(kUncategorizedFilterId);
|
final wantUncategorized = labelIds.contains(kUncategorizedFilterId);
|
||||||
if (wantUncategorized && !meta.hasLabel) return true;
|
final required = <String>[
|
||||||
for (final id in labelIds) {
|
for (final id in labelIds)
|
||||||
if (id == kUncategorizedFilterId) continue;
|
if (id != kUncategorizedFilterId) id,
|
||||||
if (meta.labelIds.contains(id)) return true;
|
];
|
||||||
|
if (wantUncategorized) {
|
||||||
|
if (required.isNotEmpty) return false;
|
||||||
|
return !meta.hasLabel;
|
||||||
}
|
}
|
||||||
return false;
|
for (final id in required) {
|
||||||
|
if (!meta.labelIds.contains(id)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Labels currently used by [transactions].
|
/// Labels currently used by [transactions].
|
||||||
|
|||||||
@@ -234,7 +234,6 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
|||||||
_period,
|
_period,
|
||||||
includedLabelIds: _includedLabelIds,
|
includedLabelIds: _includedLabelIds,
|
||||||
flow: _labelFlow,
|
flow: _labelFlow,
|
||||||
invertExpensePerspective: _labelInvertChart,
|
|
||||||
);
|
);
|
||||||
labelSpend = labels.spend;
|
labelSpend = labels.spend;
|
||||||
labelAmountByPeriod = labels.amountByPeriod;
|
labelAmountByPeriod = labels.amountByPeriod;
|
||||||
@@ -277,6 +276,7 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
|||||||
labelSpend: labelSpend,
|
labelSpend: labelSpend,
|
||||||
labelAmountByMonth: labelAmountByPeriod,
|
labelAmountByMonth: labelAmountByPeriod,
|
||||||
labelBarStyle: _labelBarStyle,
|
labelBarStyle: _labelBarStyle,
|
||||||
|
invertChart: _labelFlow == TxFlowFilter.all && _labelInvertChart,
|
||||||
currency: currency,
|
currency: currency,
|
||||||
onPickLabels: () => _pickChartLabels(context),
|
onPickLabels: () => _pickChartLabels(context),
|
||||||
includedLabelCount: _includedLabelIds.isEmpty &&
|
includedLabelCount: _includedLabelIds.isEmpty &&
|
||||||
@@ -555,8 +555,9 @@ class _LabelAggregates {
|
|||||||
/// One pass over labeled amounts: totals, per-period series, and history.
|
/// One pass over labeled amounts: totals, per-period series, and history.
|
||||||
///
|
///
|
||||||
/// Amounts are summed with sign (refunds offset spend). For [TxFlowFilter.all]
|
/// Amounts are summed with sign (refunds offset spend). For [TxFlowFilter.all]
|
||||||
/// the signed net is kept (can be negative). With [invertExpensePerspective],
|
/// the signed net is kept (can be negative). For In/Out, bars use absolute nets.
|
||||||
/// signs flip so spending plots upward. For In/Out, bars use absolute nets.
|
/// Chart-only invert is applied later in [_LabelMonthBars], not here — history
|
||||||
|
/// always follows bank transaction signs.
|
||||||
_LabelAggregates _labelAggregates(
|
_LabelAggregates _labelAggregates(
|
||||||
List<BankTransaction> txs,
|
List<BankTransaction> txs,
|
||||||
TxMetaController meta,
|
TxMetaController meta,
|
||||||
@@ -564,7 +565,6 @@ _LabelAggregates _labelAggregates(
|
|||||||
_PeriodMode mode, {
|
_PeriodMode mode, {
|
||||||
Set<String> includedLabelIds = const {},
|
Set<String> includedLabelIds = const {},
|
||||||
TxFlowFilter flow = TxFlowFilter.expense,
|
TxFlowFilter flow = TxFlowFilter.expense,
|
||||||
bool invertExpensePerspective = false,
|
|
||||||
}) {
|
}) {
|
||||||
final keys = {for (final p in periods) _periodKey(p, mode)};
|
final keys = {for (final p in periods) _periodKey(p, mode)};
|
||||||
final allByLabel = <String, double>{};
|
final allByLabel = <String, double>{};
|
||||||
@@ -576,7 +576,6 @@ _LabelAggregates _labelAggregates(
|
|||||||
};
|
};
|
||||||
final filtering = includedLabelIds.isNotEmpty;
|
final filtering = includedLabelIds.isNotEmpty;
|
||||||
final keepSigned = flow == TxFlowFilter.all;
|
final keepSigned = flow == TxFlowFilter.all;
|
||||||
final invert = keepSigned && invertExpensePerspective;
|
|
||||||
|
|
||||||
for (final tx in txs) {
|
for (final tx in txs) {
|
||||||
if (meta.metaFor(tx).internalTransfer) continue;
|
if (meta.metaFor(tx).internalTransfer) continue;
|
||||||
@@ -608,7 +607,7 @@ _LabelAggregates _labelAggregates(
|
|||||||
|
|
||||||
double displayAmount(double signed) {
|
double displayAmount(double signed) {
|
||||||
if (!keepSigned) return signed.abs();
|
if (!keepSigned) return signed.abs();
|
||||||
return invert ? -signed : signed;
|
return signed;
|
||||||
}
|
}
|
||||||
|
|
||||||
final allSpend = <_LabelSpend>[
|
final allSpend = <_LabelSpend>[
|
||||||
@@ -756,6 +755,7 @@ class _ChartCard extends StatelessWidget {
|
|||||||
required this.labelSpend,
|
required this.labelSpend,
|
||||||
required this.labelAmountByMonth,
|
required this.labelAmountByMonth,
|
||||||
required this.labelBarStyle,
|
required this.labelBarStyle,
|
||||||
|
required this.invertChart,
|
||||||
required this.currency,
|
required this.currency,
|
||||||
required this.onPickLabels,
|
required this.onPickLabels,
|
||||||
required this.includedLabelCount,
|
required this.includedLabelCount,
|
||||||
@@ -776,6 +776,7 @@ class _ChartCard extends StatelessWidget {
|
|||||||
final List<_LabelSpend> labelSpend;
|
final List<_LabelSpend> labelSpend;
|
||||||
final Map<DateTime, Map<String, double>> labelAmountByMonth;
|
final Map<DateTime, Map<String, double>> labelAmountByMonth;
|
||||||
final LabelChartBarStyle labelBarStyle;
|
final LabelChartBarStyle labelBarStyle;
|
||||||
|
final bool invertChart;
|
||||||
final String currency;
|
final String currency;
|
||||||
final VoidCallback onPickLabels;
|
final VoidCallback onPickLabels;
|
||||||
final int? includedLabelCount;
|
final int? includedLabelCount;
|
||||||
@@ -951,6 +952,7 @@ class _ChartCard extends StatelessWidget {
|
|||||||
currency: currency,
|
currency: currency,
|
||||||
period: period,
|
period: period,
|
||||||
barStyle: labelBarStyle,
|
barStyle: labelBarStyle,
|
||||||
|
invertChart: invertChart,
|
||||||
onMonthTap: onMonthTap,
|
onMonthTap: onMonthTap,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1118,6 +1120,7 @@ class _LabelMonthBars extends StatelessWidget {
|
|||||||
required this.currency,
|
required this.currency,
|
||||||
required this.period,
|
required this.period,
|
||||||
required this.barStyle,
|
required this.barStyle,
|
||||||
|
required this.invertChart,
|
||||||
required this.onMonthTap,
|
required this.onMonthTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1129,8 +1132,11 @@ class _LabelMonthBars extends StatelessWidget {
|
|||||||
final String currency;
|
final String currency;
|
||||||
final _PeriodMode period;
|
final _PeriodMode period;
|
||||||
final LabelChartBarStyle barStyle;
|
final LabelChartBarStyle barStyle;
|
||||||
|
final bool invertChart;
|
||||||
final void Function(DateTime month) onMonthTap;
|
final void Function(DateTime month) onMonthTap;
|
||||||
|
|
||||||
|
double _plot(double value) => invertChart ? -value : value;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
@@ -1172,10 +1178,11 @@ class _LabelMonthBars extends StatelessWidget {
|
|||||||
var pos = 0.0;
|
var pos = 0.0;
|
||||||
var neg = 0.0;
|
var neg = 0.0;
|
||||||
void accumulate(double value) {
|
void accumulate(double value) {
|
||||||
if (value > 0) {
|
final plotted = _plot(value);
|
||||||
pos += value;
|
if (plotted > 0) {
|
||||||
} else if (value < 0) {
|
pos += plotted;
|
||||||
neg += value;
|
} else if (plotted < 0) {
|
||||||
|
neg += plotted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1187,11 +1194,11 @@ class _LabelMonthBars extends StatelessWidget {
|
|||||||
if (neg < minY) minY = neg;
|
if (neg < minY) minY = neg;
|
||||||
} else {
|
} else {
|
||||||
for (final row in topLabels) {
|
for (final row in topLabels) {
|
||||||
final value = bucket[row.label.id] ?? 0;
|
final value = _plot(bucket[row.label.id] ?? 0);
|
||||||
if (value > maxY) maxY = value;
|
if (value > maxY) maxY = value;
|
||||||
if (value < minY) minY = value;
|
if (value < minY) minY = value;
|
||||||
}
|
}
|
||||||
final other = otherFor(month);
|
final other = _plot(otherFor(month));
|
||||||
if (other > maxY) maxY = other;
|
if (other > maxY) maxY = other;
|
||||||
if (other < minY) minY = other;
|
if (other < minY) minY = other;
|
||||||
}
|
}
|
||||||
@@ -1229,12 +1236,13 @@ class _LabelMonthBars extends StatelessWidget {
|
|||||||
var neg = 0.0;
|
var neg = 0.0;
|
||||||
|
|
||||||
void addSegment(double value, Color color) {
|
void addSegment(double value, Color color) {
|
||||||
if (value > 0) {
|
final plotted = _plot(value);
|
||||||
stackItems.add(BarChartRodStackItem(pos, pos + value, color));
|
if (plotted > 0) {
|
||||||
pos += value;
|
stackItems.add(BarChartRodStackItem(pos, pos + plotted, color));
|
||||||
} else if (value < 0) {
|
pos += plotted;
|
||||||
stackItems.add(BarChartRodStackItem(neg + value, neg, color));
|
} else if (plotted < 0) {
|
||||||
neg += value;
|
stackItems.add(BarChartRodStackItem(neg + plotted, neg, color));
|
||||||
|
neg += plotted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1277,16 +1285,16 @@ class _LabelMonthBars extends StatelessWidget {
|
|||||||
return [
|
return [
|
||||||
for (final row in topLabels)
|
for (final row in topLabels)
|
||||||
BarChartRodData(
|
BarChartRodData(
|
||||||
toY: bucket[row.label.id] ?? 0,
|
toY: _plot(bucket[row.label.id] ?? 0),
|
||||||
width: rodWidth,
|
width: rodWidth,
|
||||||
borderRadius: rodRadius(bucket[row.label.id] ?? 0),
|
borderRadius: rodRadius(_plot(bucket[row.label.id] ?? 0)),
|
||||||
color: row.label.color,
|
color: row.label.color,
|
||||||
),
|
),
|
||||||
if (hasOther)
|
if (hasOther)
|
||||||
BarChartRodData(
|
BarChartRodData(
|
||||||
toY: otherFor(month),
|
toY: _plot(otherFor(month)),
|
||||||
width: rodWidth,
|
width: rodWidth,
|
||||||
borderRadius: rodRadius(otherFor(month)),
|
borderRadius: rodRadius(_plot(otherFor(month))),
|
||||||
color: otherColor,
|
color: otherColor,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@@ -1342,7 +1350,7 @@ class _LabelMonthBars extends StatelessWidget {
|
|||||||
return BarTooltipItem(
|
return BarTooltipItem(
|
||||||
'${_periodAxisLabel(month, period)}\n'
|
'${_periodAxisLabel(month, period)}\n'
|
||||||
'$name\n'
|
'$name\n'
|
||||||
'${formatMoney(rod.toY, currency)}',
|
'${formatMoney(invertChart ? -rod.toY : rod.toY, currency)}',
|
||||||
TextStyle(color: colors.onInverseSurface),
|
TextStyle(color: colors.onInverseSurface),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1627,19 +1635,17 @@ class _LabelHistoryCard extends StatelessWidget {
|
|||||||
|
|
||||||
/// Display amount + color for history rows.
|
/// Display amount + color for history rows.
|
||||||
///
|
///
|
||||||
/// Keeps the original `-amount` sign convention; for All, color follows the
|
/// Keep the aggregate sign (All already signed; In/Out use abs then restore
|
||||||
/// displayed value (positive → green, negative → red).
|
/// bank-like sign). Color follows the displayed value.
|
||||||
(double display, Color color) _styleFor(double amount) {
|
(double display, Color color) _styleFor(double amount) {
|
||||||
switch (flow) {
|
final signed = switch (flow) {
|
||||||
case TxFlowFilter.expense:
|
TxFlowFilter.expense => -amount.abs(),
|
||||||
case TxFlowFilter.income:
|
TxFlowFilter.income => amount.abs(),
|
||||||
return (-amount.abs(), _kExpenseColor);
|
TxFlowFilter.all => amount,
|
||||||
case TxFlowFilter.all:
|
};
|
||||||
final display = -amount;
|
if (signed > 0) return (signed, _kIncomeColor);
|
||||||
if (display > 0) return (display, _kIncomeColor);
|
if (signed < 0) return (signed, _kExpenseColor);
|
||||||
if (display < 0) return (display, _kExpenseColor);
|
return (0, _kExpenseColor);
|
||||||
return (0, _kExpenseColor);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -297,7 +297,6 @@ abstract final class AppTheme {
|
|||||||
inactiveTrackColor: colorScheme.outlineVariant.withValues(alpha: 0.5),
|
inactiveTrackColor: colorScheme.outlineVariant.withValues(alpha: 0.5),
|
||||||
thumbColor: colorScheme.primary,
|
thumbColor: colorScheme.primary,
|
||||||
overlayColor: colorScheme.primary.withValues(alpha: 0.16),
|
overlayColor: colorScheme.primary.withValues(alpha: 0.16),
|
||||||
rangeThumbShape: const RoundRangeSliderThumbShape(enabledThumbRadius: 8),
|
|
||||||
trackHeight: 4,
|
trackHeight: 4,
|
||||||
),
|
),
|
||||||
checkboxTheme: CheckboxThemeData(
|
checkboxTheme: CheckboxThemeData(
|
||||||
|
|||||||
@@ -491,9 +491,8 @@ class _TransactionFilterDialogState extends State<_TransactionFilterDialog> {
|
|||||||
late final TextEditingController _queryController;
|
late final TextEditingController _queryController;
|
||||||
late final TextEditingController _labelSearchController;
|
late final TextEditingController _labelSearchController;
|
||||||
late final TextEditingController _exactAmountController;
|
late final TextEditingController _exactAmountController;
|
||||||
late double _amountCeil;
|
late final TextEditingController _minAmountController;
|
||||||
late RangeValues _amountRange;
|
late final TextEditingController _maxAmountController;
|
||||||
var _rangeTouched = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -505,28 +504,20 @@ class _TransactionFilterDialogState extends State<_TransactionFilterDialog> {
|
|||||||
_queryController = TextEditingController(text: initial.query);
|
_queryController = TextEditingController(text: initial.query);
|
||||||
_labelSearchController = TextEditingController();
|
_labelSearchController = TextEditingController();
|
||||||
|
|
||||||
var maxAbs = 0.0;
|
|
||||||
for (final tx in widget.transactions) {
|
|
||||||
final abs = tx.amount.abs();
|
|
||||||
if (abs > maxAbs) maxAbs = abs;
|
|
||||||
}
|
|
||||||
_amountCeil = maxAbs <= 0 ? 100.0 : (maxAbs * 1.05).ceilToDouble();
|
|
||||||
if (_amountCeil < 1) _amountCeil = 1;
|
|
||||||
|
|
||||||
final exact = initial.amountRange.exact;
|
final exact = initial.amountRange.exact;
|
||||||
_exactAmountController = TextEditingController(
|
_exactAmountController = TextEditingController(
|
||||||
text: exact == null ? '' : _formatAmountInput(exact),
|
text: exact == null ? '' : _formatAmountInput(exact),
|
||||||
);
|
);
|
||||||
|
_minAmountController = TextEditingController(
|
||||||
final min = initial.amountRange.min ?? 0;
|
text: exact == null && initial.amountRange.min != null
|
||||||
final max = initial.amountRange.max ?? _amountCeil;
|
? _formatAmountInput(initial.amountRange.min!)
|
||||||
_amountRange = RangeValues(
|
: '',
|
||||||
min.clamp(0, _amountCeil),
|
);
|
||||||
max.clamp(0, _amountCeil),
|
_maxAmountController = TextEditingController(
|
||||||
|
text: exact == null && initial.amountRange.max != null
|
||||||
|
? _formatAmountInput(initial.amountRange.max!)
|
||||||
|
: '',
|
||||||
);
|
);
|
||||||
_rangeTouched =
|
|
||||||
exact == null && (initial.amountRange.min != null ||
|
|
||||||
initial.amountRange.max != null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -534,6 +525,8 @@ class _TransactionFilterDialogState extends State<_TransactionFilterDialog> {
|
|||||||
_queryController.dispose();
|
_queryController.dispose();
|
||||||
_labelSearchController.dispose();
|
_labelSearchController.dispose();
|
||||||
_exactAmountController.dispose();
|
_exactAmountController.dispose();
|
||||||
|
_minAmountController.dispose();
|
||||||
|
_maxAmountController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,12 +537,21 @@ class _TransactionFilterDialogState extends State<_TransactionFilterDialog> {
|
|||||||
return value.toStringAsFixed(2);
|
return value.toStringAsFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
double? _parseExactAmount() {
|
double? _parseAmountField(TextEditingController controller) {
|
||||||
final raw = _exactAmountController.text.trim().replaceAll(',', '.');
|
final raw = controller.text.trim().replaceAll(',', '.');
|
||||||
if (raw.isEmpty) return null;
|
if (raw.isEmpty) return null;
|
||||||
return double.tryParse(raw);
|
return double.tryParse(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _clearRangeFields() {
|
||||||
|
_minAmountController.clear();
|
||||||
|
_maxAmountController.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _clearExactField() {
|
||||||
|
_exactAmountController.clear();
|
||||||
|
}
|
||||||
|
|
||||||
void _toggle(String id, bool selected) {
|
void _toggle(String id, bool selected) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
@@ -561,15 +563,18 @@ class _TransactionFilterDialogState extends State<_TransactionFilterDialog> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TxAmountRange get _amountResult {
|
TxAmountRange get _amountResult {
|
||||||
final exact = _parseExactAmount();
|
final exact = _parseAmountField(_exactAmountController);
|
||||||
if (exact != null && exact >= 0) {
|
if (exact != null && exact >= 0) {
|
||||||
return TxAmountRange(exact: exact);
|
return TxAmountRange(exact: exact);
|
||||||
}
|
}
|
||||||
if (!_rangeTouched) return const TxAmountRange();
|
final min = _parseAmountField(_minAmountController);
|
||||||
final start = _amountRange.start <= 0.009 ? null : _amountRange.start;
|
final max = _parseAmountField(_maxAmountController);
|
||||||
final end =
|
if (min == null && max == null) return const TxAmountRange();
|
||||||
_amountRange.end >= _amountCeil - 0.009 ? null : _amountRange.end;
|
// Swap if user typed max < min.
|
||||||
return TxAmountRange(min: start, max: end);
|
if (min != null && max != null && min > max) {
|
||||||
|
return TxAmountRange(min: max, max: min);
|
||||||
|
}
|
||||||
|
return TxAmountRange(min: min, max: max);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _apply() {
|
void _apply() {
|
||||||
@@ -721,7 +726,7 @@ class _TransactionFilterDialogState extends State<_TransactionFilterDialog> {
|
|||||||
: IconButton(
|
: IconButton(
|
||||||
tooltip: 'Clear exact',
|
tooltip: 'Clear exact',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_exactAmountController.clear();
|
_clearExactField();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.clear, size: 18),
|
icon: const Icon(Icons.clear, size: 18),
|
||||||
@@ -729,8 +734,9 @@ class _TransactionFilterDialogState extends State<_TransactionFilterDialog> {
|
|||||||
),
|
),
|
||||||
onChanged: (_) {
|
onChanged: (_) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_rangeTouched = false;
|
if (_exactAmountController.text.trim().isNotEmpty) {
|
||||||
_amountRange = RangeValues(0, _amountCeil);
|
_clearRangeFields();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -742,43 +748,64 @@ class _TransactionFilterDialogState extends State<_TransactionFilterDialog> {
|
|||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Row(
|
||||||
_rangeTouched
|
children: [
|
||||||
? '${_amountRange.start.toStringAsFixed(0)} – ${_amountRange.end.toStringAsFixed(0)}'
|
Expanded(
|
||||||
: 'Any amount',
|
child: TextField(
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
controller: _minAmountController,
|
||||||
color: colors.onSurfaceVariant,
|
keyboardType: const TextInputType.numberWithOptions(
|
||||||
),
|
decimal: true,
|
||||||
),
|
),
|
||||||
RangeSlider(
|
decoration: const InputDecoration(
|
||||||
values: _amountRange,
|
isDense: true,
|
||||||
min: 0,
|
labelText: 'Min',
|
||||||
max: _amountCeil,
|
hintText: '0',
|
||||||
labels: RangeLabels(
|
),
|
||||||
_amountRange.start.toStringAsFixed(0),
|
onChanged: (_) {
|
||||||
_amountRange.end.toStringAsFixed(0),
|
setState(() {
|
||||||
),
|
if (_minAmountController.text.trim().isNotEmpty ||
|
||||||
onChanged: (values) {
|
_maxAmountController.text.trim().isNotEmpty) {
|
||||||
setState(() {
|
_clearExactField();
|
||||||
_exactAmountController.clear();
|
}
|
||||||
_rangeTouched = true;
|
});
|
||||||
_amountRange = values;
|
},
|
||||||
});
|
),
|
||||||
},
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _maxAmountController,
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(
|
||||||
|
decimal: true,
|
||||||
|
),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
labelText: 'Max',
|
||||||
|
hintText: '∞',
|
||||||
|
),
|
||||||
|
onChanged: (_) {
|
||||||
|
setState(() {
|
||||||
|
if (_minAmountController.text.trim().isNotEmpty ||
|
||||||
|
_maxAmountController.text.trim().isNotEmpty) {
|
||||||
|
_clearExactField();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: !_rangeTouched
|
onPressed: _minAmountController.text.trim().isEmpty &&
|
||||||
|
_maxAmountController.text.trim().isEmpty
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
setState(() {
|
setState(_clearRangeFields);
|
||||||
_rangeTouched = false;
|
|
||||||
_amountRange = RangeValues(0, _amountCeil);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
child: const Text('Reset range'),
|
child: const Text('Clear range'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|||||||
Reference in New Issue
Block a user