new favourites feature
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
/// Named transaction-filter widgets pinned on the Home screen.
|
||||
library;
|
||||
|
||||
import '../widgets/label_filter_dialog.dart';
|
||||
import 'transaction_meta.dart';
|
||||
|
||||
/// A saved [TransactionFilterSelection] plus which metrics to show on Home.
|
||||
class HomeFilterWidget {
|
||||
const HomeFilterWidget({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.display = true,
|
||||
this.sortOrder = 0,
|
||||
this.labelIds = const [],
|
||||
this.flow = TxFlowFilter.all,
|
||||
this.amountRange = const TxAmountRange(),
|
||||
this.query = '',
|
||||
this.monthOnly = false,
|
||||
this.showAmountIn = true,
|
||||
this.showAmountOut = true,
|
||||
this.showCount = true,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final bool display;
|
||||
final int sortOrder;
|
||||
|
||||
final List<String> labelIds;
|
||||
final TxFlowFilter flow;
|
||||
final TxAmountRange amountRange;
|
||||
final String query;
|
||||
final bool monthOnly;
|
||||
|
||||
final bool showAmountIn;
|
||||
final bool showAmountOut;
|
||||
final bool showCount;
|
||||
|
||||
TransactionFilterSelection get filter => TransactionFilterSelection(
|
||||
labelIds: labelIds.toSet(),
|
||||
flow: flow,
|
||||
amountRange: amountRange,
|
||||
query: query,
|
||||
monthOnly: monthOnly,
|
||||
);
|
||||
|
||||
HomeFilterWidget copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
bool? display,
|
||||
int? sortOrder,
|
||||
List<String>? labelIds,
|
||||
TxFlowFilter? flow,
|
||||
TxAmountRange? amountRange,
|
||||
String? query,
|
||||
bool? monthOnly,
|
||||
bool? showAmountIn,
|
||||
bool? showAmountOut,
|
||||
bool? showCount,
|
||||
}) {
|
||||
return HomeFilterWidget(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
display: display ?? this.display,
|
||||
sortOrder: sortOrder ?? this.sortOrder,
|
||||
labelIds: labelIds ?? this.labelIds,
|
||||
flow: flow ?? this.flow,
|
||||
amountRange: amountRange ?? this.amountRange,
|
||||
query: query ?? this.query,
|
||||
monthOnly: monthOnly ?? this.monthOnly,
|
||||
showAmountIn: showAmountIn ?? this.showAmountIn,
|
||||
showAmountOut: showAmountOut ?? this.showAmountOut,
|
||||
showCount: showCount ?? this.showCount,
|
||||
);
|
||||
}
|
||||
|
||||
static HomeFilterWidget fromFilter({
|
||||
required String name,
|
||||
required TransactionFilterSelection filter,
|
||||
bool display = true,
|
||||
int sortOrder = 0,
|
||||
}) {
|
||||
final showIn = filter.flow != TxFlowFilter.expense;
|
||||
final showOut = filter.flow != TxFlowFilter.income;
|
||||
return HomeFilterWidget(
|
||||
id: 'hw_${DateTime.now().millisecondsSinceEpoch}',
|
||||
name: name,
|
||||
display: display,
|
||||
sortOrder: sortOrder,
|
||||
labelIds: filter.labelIds.toList(growable: false),
|
||||
flow: filter.flow,
|
||||
amountRange: filter.amountRange,
|
||||
query: filter.query,
|
||||
monthOnly: filter.monthOnly,
|
||||
showAmountIn: showIn,
|
||||
showAmountOut: showOut,
|
||||
showCount: true,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'display': display,
|
||||
'sortOrder': sortOrder,
|
||||
'labelIds': labelIds,
|
||||
'flow': flow.name,
|
||||
'amountRange': {
|
||||
if (amountRange.exact != null) 'exact': amountRange.exact,
|
||||
if (amountRange.min != null) 'min': amountRange.min,
|
||||
if (amountRange.max != null) 'max': amountRange.max,
|
||||
},
|
||||
'query': query,
|
||||
'monthOnly': monthOnly,
|
||||
'showAmountIn': showAmountIn,
|
||||
'showAmountOut': showAmountOut,
|
||||
'showCount': showCount,
|
||||
};
|
||||
|
||||
factory HomeFilterWidget.fromJson(Map<String, dynamic> json) {
|
||||
final rangeRaw = json['amountRange'];
|
||||
final rangeMap = rangeRaw is Map
|
||||
? Map<String, dynamic>.from(rangeRaw)
|
||||
: const <String, dynamic>{};
|
||||
double? asDouble(dynamic value) {
|
||||
if (value is num) return value.toDouble();
|
||||
return null;
|
||||
}
|
||||
|
||||
return HomeFilterWidget(
|
||||
id: json['id'] as String? ??
|
||||
'hw_${DateTime.now().millisecondsSinceEpoch}',
|
||||
name: (json['name'] as String?)?.trim().isNotEmpty == true
|
||||
? (json['name'] as String).trim()
|
||||
: 'Filter',
|
||||
display: json['display'] as bool? ?? true,
|
||||
sortOrder: json['sortOrder'] as int? ?? 0,
|
||||
labelIds: [
|
||||
for (final id in (json['labelIds'] as List? ?? const []))
|
||||
if (id is String && id.isNotEmpty) id,
|
||||
],
|
||||
flow: TxFlowFilter.values.firstWhere(
|
||||
(e) => e.name == json['flow'],
|
||||
orElse: () => TxFlowFilter.all,
|
||||
),
|
||||
amountRange: TxAmountRange(
|
||||
exact: asDouble(rangeMap['exact']),
|
||||
min: asDouble(rangeMap['min']),
|
||||
max: asDouble(rangeMap['max']),
|
||||
),
|
||||
query: json['query'] as String? ?? '',
|
||||
monthOnly: json['monthOnly'] as bool? ?? false,
|
||||
showAmountIn: json['showAmountIn'] as bool? ?? true,
|
||||
showAmountOut: json['showAmountOut'] as bool? ?? true,
|
||||
showCount: json['showCount'] as bool? ?? true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Union home widgets by id (local wins on conflict).
|
||||
List<HomeFilterWidget> mergeHomeFilterWidgets(
|
||||
List<HomeFilterWidget> local,
|
||||
List<HomeFilterWidget> remote,
|
||||
) {
|
||||
final byId = <String, HomeFilterWidget>{
|
||||
for (final w in remote) w.id: w,
|
||||
};
|
||||
for (final w in local) {
|
||||
byId[w.id] = w;
|
||||
}
|
||||
final merged = byId.values.toList()
|
||||
..sort((a, b) => a.sortOrder.compareTo(b.sortOrder));
|
||||
return merged;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/// Named Overview chart filter snapshots (local favourites).
|
||||
library;
|
||||
|
||||
import '../models/transaction_meta.dart';
|
||||
import '../widgets/label_filter_dialog.dart';
|
||||
|
||||
/// Persisted Overview lens: period, metric, and Labels-chart options.
|
||||
class OverviewFavourite {
|
||||
const OverviewFavourite({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.display = true,
|
||||
this.metric = OverviewMetricMode.flow,
|
||||
this.period = OverviewPeriodMode.monthly,
|
||||
this.labelIds = const [],
|
||||
this.flow = TxFlowFilter.expense,
|
||||
this.barStyle = LabelChartBarStyle.parallel,
|
||||
this.invertExpensePerspective = false,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
|
||||
/// When true, this favourite appears on the Favourites overview view.
|
||||
final bool display;
|
||||
|
||||
final OverviewMetricMode metric;
|
||||
final OverviewPeriodMode period;
|
||||
|
||||
/// Empty = all labels (Labels metric only).
|
||||
final List<String> labelIds;
|
||||
|
||||
final TxFlowFilter flow;
|
||||
final LabelChartBarStyle barStyle;
|
||||
final bool invertExpensePerspective;
|
||||
|
||||
OverviewFavourite copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
bool? display,
|
||||
OverviewMetricMode? metric,
|
||||
OverviewPeriodMode? period,
|
||||
List<String>? labelIds,
|
||||
TxFlowFilter? flow,
|
||||
LabelChartBarStyle? barStyle,
|
||||
bool? invertExpensePerspective,
|
||||
}) {
|
||||
return OverviewFavourite(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
display: display ?? this.display,
|
||||
metric: metric ?? this.metric,
|
||||
period: period ?? this.period,
|
||||
labelIds: labelIds ?? this.labelIds,
|
||||
flow: flow ?? this.flow,
|
||||
barStyle: barStyle ?? this.barStyle,
|
||||
invertExpensePerspective:
|
||||
invertExpensePerspective ?? this.invertExpensePerspective,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'display': display,
|
||||
'metric': metric.name,
|
||||
'period': period.name,
|
||||
'labelIds': labelIds,
|
||||
'flow': flow.name,
|
||||
'barStyle': barStyle.name,
|
||||
'invertExpensePerspective': invertExpensePerspective,
|
||||
};
|
||||
|
||||
factory OverviewFavourite.fromJson(Map<String, dynamic> json) {
|
||||
return OverviewFavourite(
|
||||
id: json['id'] as String? ?? 'fav_${DateTime.now().millisecondsSinceEpoch}',
|
||||
name: (json['name'] as String?)?.trim().isNotEmpty == true
|
||||
? (json['name'] as String).trim()
|
||||
: 'Favourite',
|
||||
display: json['display'] as bool? ?? true,
|
||||
metric: OverviewMetricMode.values.firstWhere(
|
||||
(e) => e.name == json['metric'],
|
||||
orElse: () => OverviewMetricMode.flow,
|
||||
),
|
||||
period: OverviewPeriodMode.values.firstWhere(
|
||||
(e) => e.name == json['period'],
|
||||
orElse: () => OverviewPeriodMode.monthly,
|
||||
),
|
||||
labelIds: [
|
||||
for (final id in (json['labelIds'] as List? ?? const []))
|
||||
if (id is String && id.isNotEmpty) id,
|
||||
],
|
||||
flow: TxFlowFilter.values.firstWhere(
|
||||
(e) => e.name == json['flow'],
|
||||
orElse: () => TxFlowFilter.expense,
|
||||
),
|
||||
barStyle: LabelChartBarStyle.values.firstWhere(
|
||||
(e) => e.name == json['barStyle'],
|
||||
orElse: () => LabelChartBarStyle.parallel,
|
||||
),
|
||||
invertExpensePerspective:
|
||||
json['invertExpensePerspective'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum OverviewMetricMode { flow, labels }
|
||||
|
||||
enum OverviewPeriodMode { monthly, yearly }
|
||||
Reference in New Issue
Block a user