stats: histogram and date filters; search: on this day filter
This commit is contained in:
parent
a3c354af0c
commit
358cf901ed
9 changed files with 316 additions and 53 deletions
|
@ -2,6 +2,11 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Search: `on this day` filter
|
||||||
|
- Stats: histogram and date filters
|
||||||
|
|
||||||
## <a id="unreleased"></a>[Unreleased]
|
## <a id="unreleased"></a>[Unreleased]
|
||||||
|
|
||||||
## <a id="v1.6.9"></a>[v1.6.9] - 2022-06-18
|
## <a id="v1.6.9"></a>[v1.6.9] - 2022-06-18
|
||||||
|
|
|
@ -122,6 +122,7 @@
|
||||||
"filterFavouriteLabel": "Favorite",
|
"filterFavouriteLabel": "Favorite",
|
||||||
"filterLocationEmptyLabel": "Unlocated",
|
"filterLocationEmptyLabel": "Unlocated",
|
||||||
"filterTagEmptyLabel": "Untagged",
|
"filterTagEmptyLabel": "Untagged",
|
||||||
|
"filterOnThisDayLabel": "On this day",
|
||||||
"filterRatingUnratedLabel": "Unrated",
|
"filterRatingUnratedLabel": "Unrated",
|
||||||
"filterRatingRejectedLabel": "Rejected",
|
"filterRatingRejectedLabel": "Rejected",
|
||||||
"filterTypeAnimatedLabel": "Animated",
|
"filterTypeAnimatedLabel": "Animated",
|
||||||
|
|
|
@ -1,91 +1,111 @@
|
||||||
import 'package:aves/model/device.dart';
|
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
import 'package:aves/theme/format.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
|
import 'package:aves/utils/time_utils.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
class LocationFilter extends CoveredCollectionFilter {
|
class DateFilter extends CoveredCollectionFilter {
|
||||||
static const type = 'location';
|
static const type = 'date';
|
||||||
static const locationSeparator = ';';
|
|
||||||
|
|
||||||
final LocationLevel level;
|
final DateLevel level;
|
||||||
late final String _location;
|
late final DateTime? date;
|
||||||
late final String? _countryCode;
|
late final DateTime _effectiveDate;
|
||||||
late final EntryFilter _test;
|
late final EntryFilter _test;
|
||||||
|
|
||||||
|
static final onThisDay = DateFilter(DateLevel.md, null);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [level, _location, _countryCode];
|
List<Object?> get props => [level, date];
|
||||||
|
|
||||||
LocationFilter(this.level, String location) {
|
DateFilter(this.level, this.date) {
|
||||||
final split = location.split(locationSeparator);
|
_effectiveDate = date ?? DateTime.now();
|
||||||
_location = split.isNotEmpty ? split[0] : location;
|
switch (level) {
|
||||||
_countryCode = split.length > 1 ? split[1] : null;
|
case DateLevel.y:
|
||||||
|
_test = (entry) => entry.bestDate?.isAtSameYearAs(_effectiveDate) ?? false;
|
||||||
if (_location.isEmpty) {
|
break;
|
||||||
_test = (entry) => !entry.hasGps;
|
case DateLevel.ym:
|
||||||
} else if (level == LocationLevel.country) {
|
_test = (entry) => entry.bestDate?.isAtSameMonthAs(_effectiveDate) ?? false;
|
||||||
_test = (entry) => entry.addressDetails?.countryCode == _countryCode;
|
break;
|
||||||
} else if (level == LocationLevel.place) {
|
case DateLevel.ymd:
|
||||||
_test = (entry) => entry.addressDetails?.place == _location;
|
_test = (entry) => entry.bestDate?.isAtSameDayAs(_effectiveDate) ?? false;
|
||||||
|
break;
|
||||||
|
case DateLevel.md:
|
||||||
|
final month = _effectiveDate.month;
|
||||||
|
final day = _effectiveDate.day;
|
||||||
|
_test = (entry) {
|
||||||
|
final bestDate = entry.bestDate;
|
||||||
|
return bestDate != null && bestDate.month == month && bestDate.day == day;
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case DateLevel.m:
|
||||||
|
final month = _effectiveDate.month;
|
||||||
|
_test = (entry) => entry.bestDate?.month == month;
|
||||||
|
break;
|
||||||
|
case DateLevel.d:
|
||||||
|
final day = _effectiveDate.day;
|
||||||
|
_test = (entry) => entry.bestDate?.day == day;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationFilter.fromMap(Map<String, dynamic> json)
|
factory DateFilter.fromMap(Map<String, dynamic> json) {
|
||||||
: this(
|
final dateString = json['date'] as String?;
|
||||||
LocationLevel.values.firstWhereOrNull((v) => v.toString() == json['level']) ?? LocationLevel.place,
|
return DateFilter(
|
||||||
json['location'],
|
DateLevel.values.firstWhereOrNull((v) => v.toString() == json['level']) ?? DateLevel.ymd,
|
||||||
);
|
dateString != null ? DateTime.tryParse(dateString) : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toMap() => {
|
Map<String, dynamic> toMap() => {
|
||||||
'type': type,
|
'type': type,
|
||||||
'level': level.toString(),
|
'level': level.toString(),
|
||||||
'location': _countryCode != null ? countryNameAndCode : _location,
|
'date': date?.toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
String get countryNameAndCode => '$_location$locationSeparator$_countryCode';
|
|
||||||
|
|
||||||
String? get countryCode => _countryCode;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
EntryFilter get test => _test;
|
EntryFilter get test => _test;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get universalLabel => _location;
|
String get universalLabel => _effectiveDate.toIso8601String();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getLabel(BuildContext context) => _location.isEmpty ? context.l10n.filterLocationEmptyLabel : _location;
|
String getLabel(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final locale = l10n.localeName;
|
||||||
|
switch (level) {
|
||||||
|
case DateLevel.y:
|
||||||
|
return DateFormat.y(locale).format(_effectiveDate);
|
||||||
|
case DateLevel.ym:
|
||||||
|
return DateFormat.yMMM(locale).format(_effectiveDate);
|
||||||
|
case DateLevel.ymd:
|
||||||
|
return formatDay(_effectiveDate, locale);
|
||||||
|
case DateLevel.md:
|
||||||
|
if (date != null) {
|
||||||
|
return DateFormat.MMMd(locale).format(_effectiveDate);
|
||||||
|
} else {
|
||||||
|
return l10n.filterOnThisDayLabel;
|
||||||
|
}
|
||||||
|
case DateLevel.m:
|
||||||
|
return DateFormat.MMMM(locale).format(_effectiveDate);
|
||||||
|
case DateLevel.d:
|
||||||
|
return DateFormat.d(locale).format(_effectiveDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) {
|
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) {
|
||||||
if (_countryCode != null && device.canRenderFlagEmojis) {
|
return Icon(AIcons.date, size: size);
|
||||||
final flag = countryCodeToFlag(_countryCode);
|
|
||||||
if (flag != null) {
|
|
||||||
return Text(
|
|
||||||
flag,
|
|
||||||
style: TextStyle(fontSize: size),
|
|
||||||
textScaleFactor: 1.0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Icon(_location.isEmpty ? AIcons.locationUnlocated : AIcons.location, size: size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get category => type;
|
String get category => type;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get key => '$type-$level-$_location';
|
String get key => '$type-$level-$date';
|
||||||
|
|
||||||
// U+0041 Latin Capital letter A
|
|
||||||
// U+1F1E6 🇦 REGIONAL INDICATOR SYMBOL LETTER A
|
|
||||||
static const _countryCodeToFlagDiff = 0x1F1E6 - 0x0041;
|
|
||||||
|
|
||||||
static String? countryCodeToFlag(String? code) {
|
|
||||||
if (code == null || code.length != 2) return null;
|
|
||||||
return String.fromCharCodes(code.toUpperCase().codeUnits.map((letter) => letter += _countryCodeToFlagDiff));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum LocationLevel { place, country }
|
enum DateLevel { y, ym, ymd, md, m, d }
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/coordinate.dart';
|
import 'package:aves/model/filters/coordinate.dart';
|
||||||
|
import 'package:aves/model/filters/date.dart';
|
||||||
import 'package:aves/model/filters/favourite.dart';
|
import 'package:aves/model/filters/favourite.dart';
|
||||||
import 'package:aves/model/filters/location.dart';
|
import 'package:aves/model/filters/location.dart';
|
||||||
import 'package:aves/model/filters/mime.dart';
|
import 'package:aves/model/filters/mime.dart';
|
||||||
|
@ -28,6 +29,7 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
||||||
MimeFilter.type,
|
MimeFilter.type,
|
||||||
AlbumFilter.type,
|
AlbumFilter.type,
|
||||||
TypeFilter.type,
|
TypeFilter.type,
|
||||||
|
DateFilter.type,
|
||||||
LocationFilter.type,
|
LocationFilter.type,
|
||||||
CoordinateFilter.type,
|
CoordinateFilter.type,
|
||||||
FavouriteFilter.type,
|
FavouriteFilter.type,
|
||||||
|
@ -52,6 +54,8 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
||||||
return AlbumFilter.fromMap(jsonMap);
|
return AlbumFilter.fromMap(jsonMap);
|
||||||
case CoordinateFilter.type:
|
case CoordinateFilter.type:
|
||||||
return CoordinateFilter.fromMap(jsonMap);
|
return CoordinateFilter.fromMap(jsonMap);
|
||||||
|
case DateFilter.type:
|
||||||
|
return DateFilter.fromMap(jsonMap);
|
||||||
case FavouriteFilter.type:
|
case FavouriteFilter.type:
|
||||||
return FavouriteFilter.instance;
|
return FavouriteFilter.instance;
|
||||||
case LocationFilter.type:
|
case LocationFilter.type:
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
|
import 'package:aves/model/filters/date.dart';
|
||||||
import 'package:aves/model/filters/favourite.dart';
|
import 'package:aves/model/filters/favourite.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/filters/location.dart';
|
import 'package:aves/model/filters/location.dart';
|
||||||
|
@ -41,6 +42,7 @@ class CollectionSearchDelegate extends AvesSearchDelegate {
|
||||||
TypeFilter.geotiff,
|
TypeFilter.geotiff,
|
||||||
TypeFilter.raw,
|
TypeFilter.raw,
|
||||||
MimeFilter(MimeTypes.svg),
|
MimeFilter(MimeTypes.svg),
|
||||||
|
DateFilter.onThisDay,
|
||||||
];
|
];
|
||||||
|
|
||||||
CollectionSearchDelegate({
|
CollectionSearchDelegate({
|
||||||
|
|
|
@ -0,0 +1,184 @@
|
||||||
|
import 'package:aves/model/entry.dart';
|
||||||
|
import 'package:aves/model/filters/date.dart';
|
||||||
|
import 'package:aves/theme/durations.dart';
|
||||||
|
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||||
|
import 'package:charts_flutter/flutter.dart' as charts;
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class Histogram extends StatefulWidget {
|
||||||
|
final Set<AvesEntry> entries;
|
||||||
|
final FilterCallback onFilterSelection;
|
||||||
|
|
||||||
|
const Histogram({
|
||||||
|
super.key,
|
||||||
|
required this.entries,
|
||||||
|
required this.onFilterSelection,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<Histogram> createState() => _HistogramState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HistogramState extends State<Histogram> {
|
||||||
|
DateLevel _level = DateLevel.y;
|
||||||
|
final Map<DateTime, int> _entryCountPerDate = {};
|
||||||
|
final ValueNotifier<EntryByDate?> _selection = ValueNotifier(null);
|
||||||
|
|
||||||
|
static const histogramHeight = 200.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
final entries = widget.entries;
|
||||||
|
final firstDate = entries.firstWhereOrNull((entry) => entry.bestDate != null)?.bestDate;
|
||||||
|
final lastDate = entries.lastWhereOrNull((entry) => entry.bestDate != null)?.bestDate;
|
||||||
|
|
||||||
|
if (lastDate != null && firstDate != null) {
|
||||||
|
final range = firstDate.difference(lastDate);
|
||||||
|
if (range > const Duration(days: 1)) {
|
||||||
|
if (range < const Duration(days: 30)) {
|
||||||
|
_level = DateLevel.ymd;
|
||||||
|
} else if (range < const Duration(days: 365)) {
|
||||||
|
_level = DateLevel.ym;
|
||||||
|
}
|
||||||
|
|
||||||
|
final dates = entries.map((entry) => entry.bestDate).whereNotNull();
|
||||||
|
late DateTime Function(DateTime) groupByKey;
|
||||||
|
switch (_level) {
|
||||||
|
case DateLevel.ymd:
|
||||||
|
groupByKey = (v) => DateTime(v.year, v.month, v.day);
|
||||||
|
break;
|
||||||
|
case DateLevel.ym:
|
||||||
|
groupByKey = (v) => DateTime(v.year, v.month);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
groupByKey = (v) => DateTime(v.year);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_entryCountPerDate.addAll(groupBy<DateTime, DateTime>(dates, groupByKey).map((k, v) => MapEntry(k, v.length)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_entryCountPerDate.isEmpty) return const SizedBox();
|
||||||
|
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
final seriesData = _entryCountPerDate.entries.map((kv) {
|
||||||
|
return EntryByDate(date: kv.key, entryCount: kv.value);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
final series = [
|
||||||
|
charts.Series<EntryByDate, DateTime>(
|
||||||
|
id: 'histogram',
|
||||||
|
colorFn: (d, i) => charts.ColorUtil.fromDartColor(theme.colorScheme.secondary),
|
||||||
|
domainFn: (d, i) => d.date,
|
||||||
|
measureFn: (d, i) => d.entryCount,
|
||||||
|
data: seriesData,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
final axisColor = charts.ColorUtil.fromDartColor(theme.colorScheme.onPrimary.withOpacity(.9));
|
||||||
|
final measureLineColor = charts.ColorUtil.fromDartColor(theme.colorScheme.onPrimary.withOpacity(.1));
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
height: histogramHeight,
|
||||||
|
child: charts.TimeSeriesChart(
|
||||||
|
series,
|
||||||
|
domainAxis: charts.DateTimeAxisSpec(
|
||||||
|
renderSpec: charts.SmallTickRendererSpec(
|
||||||
|
labelStyle: charts.TextStyleSpec(color: axisColor),
|
||||||
|
lineStyle: charts.LineStyleSpec(color: axisColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
primaryMeasureAxis: charts.NumericAxisSpec(
|
||||||
|
renderSpec: charts.GridlineRendererSpec(
|
||||||
|
labelStyle: charts.TextStyleSpec(color: axisColor),
|
||||||
|
lineStyle: charts.LineStyleSpec(color: measureLineColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
defaultRenderer: charts.BarRendererConfig<DateTime>(),
|
||||||
|
defaultInteractions: false,
|
||||||
|
behaviors: [
|
||||||
|
charts.SelectNearest(),
|
||||||
|
charts.DomainHighlighter(),
|
||||||
|
],
|
||||||
|
selectionModels: [
|
||||||
|
charts.SelectionModelConfig(
|
||||||
|
changedListener: (model) => _selection.value = model.selectedDatum.firstOrNull?.datum as EntryByDate?,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ValueListenableBuilder<EntryByDate?>(
|
||||||
|
valueListenable: _selection,
|
||||||
|
builder: (context, selection, child) {
|
||||||
|
late Widget child;
|
||||||
|
if (selection == null) {
|
||||||
|
child = const SizedBox();
|
||||||
|
} else {
|
||||||
|
final filter = DateFilter(_level, selection.date);
|
||||||
|
final count = selection.entryCount;
|
||||||
|
child = Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
AvesFilterChip(
|
||||||
|
filter: filter,
|
||||||
|
onTap: widget.onFilterSelection,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Text(
|
||||||
|
'$count',
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.textTheme.caption!.color,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnimatedSwitcher(
|
||||||
|
duration: context.read<DurationsData>().formTransition,
|
||||||
|
transitionBuilder: (child, animation) => FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: SizeTransition(
|
||||||
|
sizeFactor: animation,
|
||||||
|
axisAlignment: -1,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class EntryByDate extends Equatable {
|
||||||
|
final DateTime date;
|
||||||
|
final int entryCount;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [date, entryCount];
|
||||||
|
|
||||||
|
const EntryByDate({
|
||||||
|
required this.date,
|
||||||
|
required this.entryCount,
|
||||||
|
});
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/empty.dart';
|
import 'package:aves/widgets/common/identity/empty.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||||
import 'package:aves/widgets/stats/filter_table.dart';
|
import 'package:aves/widgets/stats/filter_table.dart';
|
||||||
|
import 'package:aves/widgets/stats/histogram.dart';
|
||||||
import 'package:charts_flutter/flutter.dart' as charts;
|
import 'package:charts_flutter/flutter.dart' as charts;
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
@ -148,6 +149,10 @@ class StatsPage extends StatelessWidget {
|
||||||
child = ListView(
|
child = ListView(
|
||||||
children: [
|
children: [
|
||||||
mimeDonuts,
|
mimeDonuts,
|
||||||
|
Histogram(
|
||||||
|
entries: entries,
|
||||||
|
onFilterSelection: (filter) => _onFilterSelection(context, filter),
|
||||||
|
),
|
||||||
locationIndicator,
|
locationIndicator,
|
||||||
..._buildFilterSection<String>(context, context.l10n.statsTopCountries, entryCountPerCountry, (v) => LocationFilter(LocationLevel.country, v)),
|
..._buildFilterSection<String>(context, context.l10n.statsTopCountries, entryCountPerCountry, (v) => LocationFilter(LocationLevel.country, v)),
|
||||||
..._buildFilterSection<String>(context, context.l10n.statsTopPlaces, entryCountPerPlace, (v) => LocationFilter(LocationLevel.place, v)),
|
..._buildFilterSection<String>(context, context.l10n.statsTopPlaces, entryCountPerPlace, (v) => LocationFilter(LocationLevel.place, v)),
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/coordinate.dart';
|
import 'package:aves/model/filters/coordinate.dart';
|
||||||
|
import 'package:aves/model/filters/date.dart';
|
||||||
import 'package:aves/model/filters/favourite.dart';
|
import 'package:aves/model/filters/favourite.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/filters/location.dart';
|
import 'package:aves/model/filters/location.dart';
|
||||||
|
@ -36,6 +37,12 @@ void main() {
|
||||||
final bounds = CoordinateFilter(LatLng(29.979167, 28.223615), LatLng(36.451000, 31.134167));
|
final bounds = CoordinateFilter(LatLng(29.979167, 28.223615), LatLng(36.451000, 31.134167));
|
||||||
expect(bounds, jsonRoundTrip(bounds));
|
expect(bounds, jsonRoundTrip(bounds));
|
||||||
|
|
||||||
|
final date = DateFilter(DateLevel.ym, DateTime(1969, 7));
|
||||||
|
expect(date, jsonRoundTrip(date));
|
||||||
|
|
||||||
|
final onThisDay = DateFilter.onThisDay;
|
||||||
|
expect(onThisDay, jsonRoundTrip(onThisDay));
|
||||||
|
|
||||||
const fav = FavouriteFilter.instance;
|
const fav = FavouriteFilter.instance;
|
||||||
expect(fav, jsonRoundTrip(fav));
|
expect(fav, jsonRoundTrip(fav));
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,28 @@
|
||||||
{
|
{
|
||||||
|
"de": [
|
||||||
|
"filterOnThisDayLabel"
|
||||||
|
],
|
||||||
|
|
||||||
|
"es": [
|
||||||
|
"filterOnThisDayLabel"
|
||||||
|
],
|
||||||
|
|
||||||
|
"fr": [
|
||||||
|
"filterOnThisDayLabel"
|
||||||
|
],
|
||||||
|
|
||||||
|
"id": [
|
||||||
|
"filterOnThisDayLabel"
|
||||||
|
],
|
||||||
|
|
||||||
|
"it": [
|
||||||
|
"filterOnThisDayLabel"
|
||||||
|
],
|
||||||
|
|
||||||
"ja": [
|
"ja": [
|
||||||
"slideshowActionResume",
|
"slideshowActionResume",
|
||||||
"slideshowActionShowInCollection",
|
"slideshowActionShowInCollection",
|
||||||
|
"filterOnThisDayLabel",
|
||||||
"slideshowVideoPlaybackSkip",
|
"slideshowVideoPlaybackSkip",
|
||||||
"slideshowVideoPlaybackMuted",
|
"slideshowVideoPlaybackMuted",
|
||||||
"slideshowVideoPlaybackWithSound",
|
"slideshowVideoPlaybackWithSound",
|
||||||
|
@ -28,9 +49,18 @@
|
||||||
"viewerSetWallpaperButtonLabel"
|
"viewerSetWallpaperButtonLabel"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
"ko": [
|
||||||
|
"filterOnThisDayLabel"
|
||||||
|
],
|
||||||
|
|
||||||
|
"pt": [
|
||||||
|
"filterOnThisDayLabel"
|
||||||
|
],
|
||||||
|
|
||||||
"ru": [
|
"ru": [
|
||||||
"slideshowActionResume",
|
"slideshowActionResume",
|
||||||
"slideshowActionShowInCollection",
|
"slideshowActionShowInCollection",
|
||||||
|
"filterOnThisDayLabel",
|
||||||
"slideshowVideoPlaybackSkip",
|
"slideshowVideoPlaybackSkip",
|
||||||
"slideshowVideoPlaybackMuted",
|
"slideshowVideoPlaybackMuted",
|
||||||
"slideshowVideoPlaybackWithSound",
|
"slideshowVideoPlaybackWithSound",
|
||||||
|
@ -60,6 +90,7 @@
|
||||||
"tr": [
|
"tr": [
|
||||||
"slideshowActionResume",
|
"slideshowActionResume",
|
||||||
"slideshowActionShowInCollection",
|
"slideshowActionShowInCollection",
|
||||||
|
"filterOnThisDayLabel",
|
||||||
"slideshowVideoPlaybackSkip",
|
"slideshowVideoPlaybackSkip",
|
||||||
"slideshowVideoPlaybackMuted",
|
"slideshowVideoPlaybackMuted",
|
||||||
"slideshowVideoPlaybackWithSound",
|
"slideshowVideoPlaybackWithSound",
|
||||||
|
@ -82,5 +113,9 @@
|
||||||
"settingsSlideshowVideoPlaybackTile",
|
"settingsSlideshowVideoPlaybackTile",
|
||||||
"settingsSlideshowVideoPlaybackTitle",
|
"settingsSlideshowVideoPlaybackTitle",
|
||||||
"viewerSetWallpaperButtonLabel"
|
"viewerSetWallpaperButtonLabel"
|
||||||
|
],
|
||||||
|
|
||||||
|
"zh": [
|
||||||
|
"filterOnThisDayLabel"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue