stats: top countries and tags
This commit is contained in:
parent
c8b8d9c897
commit
ef130eb820
3 changed files with 102 additions and 5 deletions
|
@ -4,10 +4,10 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class FilterBar extends StatelessWidget implements PreferredSizeWidget {
|
class FilterBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
static final double preferredHeight = kMinInteractiveDimension;
|
static const double preferredHeight = kMinInteractiveDimension;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final Size preferredSize = Size.fromHeight(preferredHeight);
|
final Size preferredSize = const Size.fromHeight(preferredHeight);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
@ -4,6 +4,8 @@ import 'package:outline_material_icons/outline_material_icons.dart';
|
||||||
|
|
||||||
typedef FilterCallback = void Function(CollectionFilter filter);
|
typedef FilterCallback = void Function(CollectionFilter filter);
|
||||||
|
|
||||||
|
typedef FilterBuilder = CollectionFilter Function(String label);
|
||||||
|
|
||||||
class AvesFilterChip extends StatefulWidget {
|
class AvesFilterChip extends StatefulWidget {
|
||||||
final CollectionFilter filter;
|
final CollectionFilter filter;
|
||||||
final bool removable;
|
final bool removable;
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
import 'package:aves/model/collection_lens.dart';
|
import 'package:aves/model/collection_lens.dart';
|
||||||
|
import 'package:aves/model/filters/country.dart';
|
||||||
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
import 'package:aves/model/filters/tag.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/utils/color_utils.dart';
|
import 'package:aves/utils/color_utils.dart';
|
||||||
|
import 'package:aves/utils/constants.dart';
|
||||||
|
import 'package:aves/widgets/album/collection_page.dart';
|
||||||
|
import 'package:aves/widgets/common/aves_filter_chip.dart';
|
||||||
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/data_providers/media_query_data_provider.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';
|
||||||
|
@ -11,8 +17,19 @@ import 'package:percent_indicator/linear_percent_indicator.dart';
|
||||||
|
|
||||||
class StatsPage extends StatelessWidget {
|
class StatsPage extends StatelessWidget {
|
||||||
final CollectionLens collection;
|
final CollectionLens collection;
|
||||||
|
final Map<String, int> entryCountPerCountry = Map<String, int>(), entryCountPerTag = Map<String, int>();
|
||||||
|
|
||||||
const StatsPage({this.collection});
|
StatsPage({this.collection}) {
|
||||||
|
entries.forEach((entry) {
|
||||||
|
final country = entry.addressDetails?.countryName;
|
||||||
|
if (country != null) {
|
||||||
|
entryCountPerCountry[country] = (entryCountPerCountry[country] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
entry.xmpSubjects.forEach((tag) {
|
||||||
|
entryCountPerTag[tag] = (entryCountPerTag[tag] ?? 0) + 1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
List<ImageEntry> get entries => collection.sortedEntries;
|
List<ImageEntry> get entries => collection.sortedEntries;
|
||||||
|
|
||||||
|
@ -20,7 +37,7 @@ class StatsPage extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final catalogued = entries.where((entry) => entry.isCatalogued);
|
final catalogued = entries.where((entry) => entry.isCatalogued);
|
||||||
final withGps = catalogued.where((entry) => entry.hasGps);
|
final withGps = catalogued.where((entry) => entry.hasGps);
|
||||||
final withGpsPercent = withGps.length / entries.length;
|
final withGpsPercent = withGps.length / collection.entryCount;
|
||||||
final Map<String, int> byMimeTypes = groupBy(entries, (entry) => entry.mimeType).map((k, v) => MapEntry(k, v.length));
|
final Map<String, int> byMimeTypes = groupBy(entries, (entry) => entry.mimeType).map((k, v) => MapEntry(k, v.length));
|
||||||
final imagesByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('image/')));
|
final imagesByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('image/')));
|
||||||
final videoByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('video/')));
|
final videoByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('video/')));
|
||||||
|
@ -59,6 +76,8 @@ class StatsPage extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
..._buildTopFilters(context, 'Top countries', entryCountPerCountry, (s) => CountryFilter(s)),
|
||||||
|
..._buildTopFilters(context, 'Top tags', entryCountPerTag, (s) => TagFilter(s)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -72,7 +91,10 @@ class StatsPage extends StatelessWidget {
|
||||||
final sum = byMimeTypes.values.fold(0, (prev, v) => prev + v);
|
final sum = byMimeTypes.values.fold(0, (prev, v) => prev + v);
|
||||||
|
|
||||||
final seriesData = byMimeTypes.entries.map((kv) => StringNumDatum(kv.key.replaceFirst(RegExp('.*/'), '').toUpperCase(), kv.value)).toList();
|
final seriesData = byMimeTypes.entries.map((kv) => StringNumDatum(kv.key.replaceFirst(RegExp('.*/'), '').toUpperCase(), kv.value)).toList();
|
||||||
seriesData.sort((kv1, kv2) => kv2.value.compareTo(kv1.value));
|
seriesData.sort((kv1, kv2) {
|
||||||
|
final c = kv2.value.compareTo(kv1.value);
|
||||||
|
return c != 0 ? c : compareAsciiUpperCase(kv1.key, kv2.key);
|
||||||
|
});
|
||||||
|
|
||||||
final series = [
|
final series = [
|
||||||
charts.Series<StringNumDatum, String>(
|
charts.Series<StringNumDatum, String>(
|
||||||
|
@ -132,6 +154,79 @@ class StatsPage extends StatelessWidget {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildTopFilters(BuildContext context, String title, Map<String, int> entryCountMap, FilterBuilder filterBuilder) {
|
||||||
|
if (entryCountMap.isEmpty) return [];
|
||||||
|
|
||||||
|
final maxCount = collection.entryCount;
|
||||||
|
final sortedEntries = entryCountMap.entries.toList()
|
||||||
|
..sort((kv1, kv2) {
|
||||||
|
final c = kv2.value.compareTo(kv1.value);
|
||||||
|
return c != 0 ? c : compareAsciiUpperCase(kv1.key, kv2.key);
|
||||||
|
});
|
||||||
|
return [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: Constants.titleTextStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.only(start: AvesFilterChip.buttonBorderWidth / 2 + 6, end: 8),
|
||||||
|
child: Table(
|
||||||
|
children: sortedEntries.take(5).map((kv) {
|
||||||
|
final label = kv.key;
|
||||||
|
final count = kv.value;
|
||||||
|
final percent = count / maxCount;
|
||||||
|
return TableRow(
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
child: AvesFilterChip(
|
||||||
|
filter: filterBuilder(label),
|
||||||
|
onPressed: (filter) => _goToFilteredCollection(context, filter),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: LinearPercentIndicator(
|
||||||
|
percent: percent,
|
||||||
|
lineHeight: 16,
|
||||||
|
backgroundColor: Colors.white24,
|
||||||
|
progressColor: stringToColor(label),
|
||||||
|
animation: true,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
center: Text(NumberFormat.percentPattern().format(percent)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${count}',
|
||||||
|
style: const TextStyle(color: Colors.white70),
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
columnWidths: const {
|
||||||
|
0: IntrinsicColumnWidth(),
|
||||||
|
2: IntrinsicColumnWidth(),
|
||||||
|
},
|
||||||
|
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
void _goToFilteredCollection(BuildContext context, CollectionFilter filter) {
|
||||||
|
if (collection == null) return;
|
||||||
|
Navigator.pushAndRemoveUntil(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => CollectionPage(collection.derive(filter)),
|
||||||
|
),
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StringNumDatum {
|
class StringNumDatum {
|
||||||
|
|
Loading…
Reference in a new issue