fix tap on chart
This commit is contained in:
@@ -487,6 +487,16 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
|||||||
: null),
|
: null),
|
||||||
periodMode: favourite.period,
|
periodMode: favourite.period,
|
||||||
),
|
),
|
||||||
|
onChartTap: () => _openChartFilter(
|
||||||
|
context,
|
||||||
|
labelIds: {
|
||||||
|
for (final id in favourite.labelIds) id,
|
||||||
|
},
|
||||||
|
flow: favourite.metric == OverviewMetricMode.labels
|
||||||
|
? favourite.flow
|
||||||
|
: null,
|
||||||
|
periodMode: favourite.period,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,6 +619,10 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
|||||||
flow: flow ??
|
flow: flow ??
|
||||||
(_metric == OverviewMetricMode.labels ? _labelFlow : null),
|
(_metric == OverviewMetricMode.labels ? _labelFlow : null),
|
||||||
),
|
),
|
||||||
|
onChartTap: () => _openChartFilter(
|
||||||
|
context,
|
||||||
|
flow: _metric == OverviewMetricMode.labels ? _labelFlow : null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
if (_metric == OverviewMetricMode.flow) ...[
|
if (_metric == OverviewMetricMode.flow) ...[
|
||||||
@@ -784,6 +798,34 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open Transactions with this chart's filters (no specific bar/column).
|
||||||
|
void _openChartFilter(
|
||||||
|
BuildContext context, {
|
||||||
|
Set<String>? labelIds,
|
||||||
|
TxFlowFilter? flow,
|
||||||
|
OverviewPeriodMode? periodMode,
|
||||||
|
}) {
|
||||||
|
final mode = periodMode ?? _period;
|
||||||
|
final yearly = mode == OverviewPeriodMode.yearly;
|
||||||
|
final resolvedLabelIds = labelIds ??
|
||||||
|
{
|
||||||
|
for (final id in _includedLabelIds) id,
|
||||||
|
};
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (_) => TransactionsScreen(
|
||||||
|
// Monthly charts show a year — open that whole year with the chart filter.
|
||||||
|
initialMonth: yearly ? null : DateTime(_cursor.year, 12),
|
||||||
|
initialYear: yearly ? null : _cursor.year,
|
||||||
|
initialLabelIds: resolvedLabelIds,
|
||||||
|
initialLabelMatch: LabelMatchMode.or,
|
||||||
|
initialFlow: flow,
|
||||||
|
initialSearchInMonthOnly: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MonthBucket {
|
class _MonthBucket {
|
||||||
@@ -1183,6 +1225,7 @@ class _ChartCard extends StatelessWidget {
|
|||||||
required this.availableLabelCount,
|
required this.availableLabelCount,
|
||||||
required this.onLabelTap,
|
required this.onLabelTap,
|
||||||
required this.onMonthTap,
|
required this.onMonthTap,
|
||||||
|
required this.onChartTap,
|
||||||
this.onPeriodModeChanged,
|
this.onPeriodModeChanged,
|
||||||
this.onEdit,
|
this.onEdit,
|
||||||
});
|
});
|
||||||
@@ -1211,6 +1254,7 @@ class _ChartCard extends StatelessWidget {
|
|||||||
String? labelId,
|
String? labelId,
|
||||||
TxFlowFilter? flow,
|
TxFlowFilter? flow,
|
||||||
}) onMonthTap;
|
}) onMonthTap;
|
||||||
|
final VoidCallback onChartTap;
|
||||||
final ValueChanged<OverviewPeriodMode>? onPeriodModeChanged;
|
final ValueChanged<OverviewPeriodMode>? onPeriodModeChanged;
|
||||||
final VoidCallback? onEdit;
|
final VoidCallback? onEdit;
|
||||||
|
|
||||||
@@ -1551,6 +1595,8 @@ class _ChartCard extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: chartHeight,
|
height: chartHeight,
|
||||||
|
child: _TappableChartArea(
|
||||||
|
onChartTap: onChartTap,
|
||||||
child: metric == OverviewMetricMode.flow
|
child: metric == OverviewMetricMode.flow
|
||||||
? _FlowBars(
|
? _FlowBars(
|
||||||
months: months,
|
months: months,
|
||||||
@@ -1558,6 +1604,7 @@ class _ChartCard extends StatelessWidget {
|
|||||||
currency: currency,
|
currency: currency,
|
||||||
period: period,
|
period: period,
|
||||||
onMonthTap: onMonthTap,
|
onMonthTap: onMonthTap,
|
||||||
|
onChartTap: onChartTap,
|
||||||
)
|
)
|
||||||
: _LabelMonthBars(
|
: _LabelMonthBars(
|
||||||
months: months,
|
months: months,
|
||||||
@@ -1568,6 +1615,8 @@ class _ChartCard extends StatelessWidget {
|
|||||||
barStyle: labelBarStyle,
|
barStyle: labelBarStyle,
|
||||||
invertChart: invertChart,
|
invertChart: invertChart,
|
||||||
onMonthTap: onMonthTap,
|
onMonthTap: onMonthTap,
|
||||||
|
onChartTap: onChartTap,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1577,6 +1626,69 @@ class _ChartCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lets empty-chart taps open the chart filter without double-firing when a bar
|
||||||
|
/// was just selected (fl_chart + parent [GestureDetector] both see the tap).
|
||||||
|
class _TappableChartArea extends StatefulWidget {
|
||||||
|
const _TappableChartArea({
|
||||||
|
required this.onChartTap,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
final VoidCallback onChartTap;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_TappableChartArea> createState() => _TappableChartAreaState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TappableChartAreaState extends State<_TappableChartArea> {
|
||||||
|
var _handling = false;
|
||||||
|
|
||||||
|
void _runExclusive(VoidCallback action) {
|
||||||
|
if (_handling) return;
|
||||||
|
_handling = true;
|
||||||
|
action();
|
||||||
|
Future<void>.delayed(const Duration(milliseconds: 400), () {
|
||||||
|
if (mounted) _handling = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void openChartFilter() => _runExclusive(widget.onChartTap);
|
||||||
|
|
||||||
|
void openBar(VoidCallback action) => _runExclusive(action);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: openChartFilter,
|
||||||
|
child: _ChartTapScope(
|
||||||
|
openChartFilter: openChartFilter,
|
||||||
|
openBar: openBar,
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChartTapScope extends InheritedWidget {
|
||||||
|
const _ChartTapScope({
|
||||||
|
required this.openChartFilter,
|
||||||
|
required this.openBar,
|
||||||
|
required super.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
final VoidCallback openChartFilter;
|
||||||
|
final void Function(VoidCallback action) openBar;
|
||||||
|
|
||||||
|
static _ChartTapScope? maybeOf(BuildContext context) {
|
||||||
|
return context.getInheritedWidgetOfExactType<_ChartTapScope>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(_ChartTapScope oldWidget) => false;
|
||||||
|
}
|
||||||
|
|
||||||
class _FlowBars extends StatelessWidget {
|
class _FlowBars extends StatelessWidget {
|
||||||
const _FlowBars({
|
const _FlowBars({
|
||||||
required this.months,
|
required this.months,
|
||||||
@@ -1584,6 +1696,7 @@ class _FlowBars extends StatelessWidget {
|
|||||||
required this.currency,
|
required this.currency,
|
||||||
required this.period,
|
required this.period,
|
||||||
required this.onMonthTap,
|
required this.onMonthTap,
|
||||||
|
required this.onChartTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<DateTime> months;
|
final List<DateTime> months;
|
||||||
@@ -1595,6 +1708,7 @@ class _FlowBars extends StatelessWidget {
|
|||||||
String? labelId,
|
String? labelId,
|
||||||
TxFlowFilter? flow,
|
TxFlowFilter? flow,
|
||||||
}) onMonthTap;
|
}) onMonthTap;
|
||||||
|
final VoidCallback onChartTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -1613,7 +1727,10 @@ class _FlowBars extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (empty) {
|
if (empty) {
|
||||||
return Center(
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: onChartTap,
|
||||||
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
period == OverviewPeriodMode.yearly
|
period == OverviewPeriodMode.yearly
|
||||||
? 'No income or expenses in these years'
|
? 'No income or expenses in these years'
|
||||||
@@ -1621,6 +1738,7 @@ class _FlowBars extends StatelessWidget {
|
|||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1632,15 +1750,25 @@ class _FlowBars extends StatelessWidget {
|
|||||||
touchCallback: (event, response) {
|
touchCallback: (event, response) {
|
||||||
if (event is! FlTapUpEvent) return;
|
if (event is! FlTapUpEvent) return;
|
||||||
final spot = response?.spot;
|
final spot = response?.spot;
|
||||||
if (spot == null) return;
|
final index = spot?.touchedBarGroupIndex;
|
||||||
final index = spot.touchedBarGroupIndex;
|
if (spot == null ||
|
||||||
if (index < 0 || index >= months.length) return;
|
index == null ||
|
||||||
|
index < 0 ||
|
||||||
|
index >= months.length) {
|
||||||
|
// Parent GestureDetector also handles empty-area taps.
|
||||||
|
return;
|
||||||
|
}
|
||||||
final rodIndex = spot.touchedRodDataIndex;
|
final rodIndex = spot.touchedRodDataIndex;
|
||||||
// Rods: 0 = expense, 1 = income.
|
// Rods: 0 = expense, 1 = income.
|
||||||
final flow = rodIndex == 1
|
final flow = rodIndex == 1
|
||||||
? TxFlowFilter.income
|
? TxFlowFilter.income
|
||||||
: TxFlowFilter.expense;
|
: TxFlowFilter.expense;
|
||||||
|
final scope = _ChartTapScope.maybeOf(context);
|
||||||
|
if (scope != null) {
|
||||||
|
scope.openBar(() => onMonthTap(months[index], flow: flow));
|
||||||
|
} else {
|
||||||
onMonthTap(months[index], flow: flow);
|
onMonthTap(months[index], flow: flow);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
touchTooltipData: BarTouchTooltipData(
|
touchTooltipData: BarTouchTooltipData(
|
||||||
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
||||||
@@ -1755,6 +1883,7 @@ class _LabelMonthBars extends StatelessWidget {
|
|||||||
required this.barStyle,
|
required this.barStyle,
|
||||||
required this.invertChart,
|
required this.invertChart,
|
||||||
required this.onMonthTap,
|
required this.onMonthTap,
|
||||||
|
required this.onChartTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
static const _maxSeriesLabels = 6;
|
static const _maxSeriesLabels = 6;
|
||||||
@@ -1771,6 +1900,7 @@ class _LabelMonthBars extends StatelessWidget {
|
|||||||
String? labelId,
|
String? labelId,
|
||||||
TxFlowFilter? flow,
|
TxFlowFilter? flow,
|
||||||
}) onMonthTap;
|
}) onMonthTap;
|
||||||
|
final VoidCallback onChartTap;
|
||||||
|
|
||||||
double _plot(double value) => invertChart ? -value : value;
|
double _plot(double value) => invertChart ? -value : value;
|
||||||
|
|
||||||
@@ -1779,12 +1909,16 @@ class _LabelMonthBars extends StatelessWidget {
|
|||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final colors = theme.colorScheme;
|
final colors = theme.colorScheme;
|
||||||
if (labelSpend.isEmpty) {
|
if (labelSpend.isEmpty) {
|
||||||
return Center(
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: onChartTap,
|
||||||
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'No labeled activity in this period.\nTag transactions to see them here.',
|
'No labeled activity in this period.\nTag transactions to see them here.',
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1841,12 +1975,16 @@ class _LabelMonthBars extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (maxY == 0 && minY == 0) {
|
if (maxY == 0 && minY == 0) {
|
||||||
return Center(
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: onChartTap,
|
||||||
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'No labeled activity in this period.\nTag transactions to see them here.',
|
'No labeled activity in this period.\nTag transactions to see them here.',
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final span = (maxY - minY).abs();
|
final span = (maxY - minY).abs();
|
||||||
@@ -1949,9 +2087,14 @@ class _LabelMonthBars extends StatelessWidget {
|
|||||||
touchCallback: (event, response) {
|
touchCallback: (event, response) {
|
||||||
if (event is! FlTapUpEvent) return;
|
if (event is! FlTapUpEvent) return;
|
||||||
final spot = response?.spot;
|
final spot = response?.spot;
|
||||||
if (spot == null) return;
|
final index = spot?.touchedBarGroupIndex;
|
||||||
final index = spot.touchedBarGroupIndex;
|
if (spot == null ||
|
||||||
if (index < 0 || index >= months.length) return;
|
index == null ||
|
||||||
|
index < 0 ||
|
||||||
|
index >= months.length) {
|
||||||
|
// Parent GestureDetector also handles empty-area taps.
|
||||||
|
return;
|
||||||
|
}
|
||||||
String? labelId;
|
String? labelId;
|
||||||
if (stacked) {
|
if (stacked) {
|
||||||
final stackIdx = spot.touchedStackItemIndex;
|
final stackIdx = spot.touchedStackItemIndex;
|
||||||
@@ -1965,7 +2108,14 @@ class _LabelMonthBars extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
// Last rod may be "Other" — open month with chart labels only.
|
// Last rod may be "Other" — open month with chart labels only.
|
||||||
}
|
}
|
||||||
|
final scope = _ChartTapScope.maybeOf(context);
|
||||||
|
if (scope != null) {
|
||||||
|
scope.openBar(
|
||||||
|
() => onMonthTap(months[index], labelId: labelId),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
onMonthTap(months[index], labelId: labelId);
|
onMonthTap(months[index], labelId: labelId);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
touchTooltipData: BarTouchTooltipData(
|
touchTooltipData: BarTouchTooltipData(
|
||||||
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
||||||
|
|||||||
Reference in New Issue
Block a user