From aa99129abfed9a41d382a10b43c7c4f1e97a2c45 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 21 Apr 2020 10:45:10 +0900 Subject: [PATCH] stats: improved mime legend, filter table layout --- lib/main.dart | 2 +- lib/widgets/album/app_bar.dart | 8 +- lib/widgets/album/search/search_delegate.dart | 2 +- lib/widgets/common/aves_filter_chip.dart | 2 +- lib/widgets/common/menu_row.dart | 2 +- lib/widgets/common/scroll_thumb.dart | 4 +- .../fullscreen/info/location_section.dart | 6 +- lib/widgets/fullscreen/overlay/top.dart | 2 +- lib/widgets/fullscreen/overlay/video.dart | 2 +- lib/widgets/stats/filter_table.dart | 94 +++++++++++++++++++ lib/widgets/{ => stats}/stats.dart | 78 +++------------ 11 files changed, 123 insertions(+), 79 deletions(-) create mode 100644 lib/widgets/stats/filter_table.dart rename lib/widgets/{ => stats}/stats.dart (73%) diff --git a/lib/main.dart b/lib/main.dart index d1e119baa..dd5148689 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,7 +26,7 @@ class AvesApp extends StatelessWidget { brightness: Brightness.dark, accentColor: Colors.indigoAccent, scaffoldBackgroundColor: Colors.grey[900], - appBarTheme: AppBarTheme( + appBarTheme: const AppBarTheme( textTheme: TextTheme( headline6: TextStyle( fontSize: 20, diff --git a/lib/widgets/album/app_bar.dart b/lib/widgets/album/app_bar.dart index 4edee321f..f40668c30 100644 --- a/lib/widgets/album/app_bar.dart +++ b/lib/widgets/album/app_bar.dart @@ -6,7 +6,7 @@ import 'package:aves/widgets/album/collection_page.dart'; import 'package:aves/widgets/album/filter_bar.dart'; import 'package:aves/widgets/album/search/search_delegate.dart'; import 'package:aves/widgets/common/menu_row.dart'; -import 'package:aves/widgets/stats.dart'; +import 'package:aves/widgets/stats/stats.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:outline_material_icons/outline_material_icons.dart'; @@ -149,12 +149,12 @@ class _CollectionAppBarState extends State with SingleTickerPr switch (stateNotifier.value) { case PageState.browse: return IconButton( - icon: Icon(OMIcons.search), + icon: const Icon(OMIcons.search), onPressed: _goToSearch, ); case PageState.search: return IconButton( - icon: Icon(OMIcons.clear), + icon: const Icon(OMIcons.clear), onPressed: () => _searchFieldController.clear(), ); } @@ -192,7 +192,7 @@ class _CollectionAppBarState extends State with SingleTickerPr ), const PopupMenuDivider(), ], - PopupMenuItem( + const PopupMenuItem( value: CollectionAction.stats, child: MenuRow(text: 'Stats', icon: OMIcons.pieChart), ), diff --git a/lib/widgets/album/search/search_delegate.dart b/lib/widgets/album/search/search_delegate.dart index 0a02e4880..556c5ef83 100644 --- a/lib/widgets/album/search/search_delegate.dart +++ b/lib/widgets/album/search/search_delegate.dart @@ -40,7 +40,7 @@ class ImageSearchDelegate extends SearchDelegate { if (query.isNotEmpty) IconButton( tooltip: 'Clear', - icon: Icon(OMIcons.clear), + icon: const Icon(OMIcons.clear), onPressed: () { query = ''; showSuggestions(context); diff --git a/lib/widgets/common/aves_filter_chip.dart b/lib/widgets/common/aves_filter_chip.dart index ca16b64a8..e6ab0cd29 100644 --- a/lib/widgets/common/aves_filter_chip.dart +++ b/lib/widgets/common/aves_filter_chip.dart @@ -49,7 +49,7 @@ class _AvesFilterChipState extends State { @override Widget build(BuildContext context) { final leading = filter.iconBuilder(context, AvesFilterChip.iconSize); - final trailing = widget.removable ? Icon(OMIcons.clear, size: AvesFilterChip.iconSize) : null; + final trailing = widget.removable ? const Icon(OMIcons.clear, size: AvesFilterChip.iconSize) : null; final child = Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/widgets/common/menu_row.dart b/lib/widgets/common/menu_row.dart index 98dec8086..4ca157173 100644 --- a/lib/widgets/common/menu_row.dart +++ b/lib/widgets/common/menu_row.dart @@ -20,7 +20,7 @@ class MenuRow extends StatelessWidget { if (checked != null) ...[ Opacity( opacity: checked ? 1 : 0, - child: Icon(OMIcons.done), + child: const Icon(OMIcons.done), ), const SizedBox(width: 8), ], diff --git a/lib/widgets/common/scroll_thumb.dart b/lib/widgets/common/scroll_thumb.dart index 5c41312a9..0c21bb256 100644 --- a/lib/widgets/common/scroll_thumb.dart +++ b/lib/widgets/common/scroll_thumb.dart @@ -10,9 +10,9 @@ ScrollThumbBuilder avesScrollThumbBuilder({ @required Color backgroundColor, }) { final scrollThumb = Container( - decoration: BoxDecoration( + decoration: const BoxDecoration( color: Colors.black26, - borderRadius: const BorderRadius.all( + borderRadius: BorderRadius.all( Radius.circular(12.0), ), ), diff --git a/lib/widgets/fullscreen/info/location_section.dart b/lib/widgets/fullscreen/info/location_section.dart index c0a452ab9..a4d37f677 100644 --- a/lib/widgets/fullscreen/info/location_section.dart +++ b/lib/widgets/fullscreen/info/location_section.dart @@ -211,17 +211,17 @@ class ImageMapState extends State with AutomaticKeepAliveClientMixin { const SizedBox(width: 8), Column(children: [ IconButton( - icon: Icon(OMIcons.add), + icon: const Icon(OMIcons.add), onPressed: _controller == null ? null : () => _zoomBy(1), tooltip: 'Zoom in', ), IconButton( - icon: Icon(OMIcons.remove), + icon: const Icon(OMIcons.remove), onPressed: _controller == null ? null : () => _zoomBy(-1), tooltip: 'Zoom out', ), IconButton( - icon: Icon(OMIcons.openInNew), + icon: const Icon(OMIcons.openInNew), onPressed: () => AndroidAppService.openMap(widget.geoUri), tooltip: 'Show on map...', ), diff --git a/lib/widgets/fullscreen/overlay/top.dart b/lib/widgets/fullscreen/overlay/top.dart index 2059b3e8c..e0d099ae0 100644 --- a/lib/widgets/fullscreen/overlay/top.dart +++ b/lib/widgets/fullscreen/overlay/top.dart @@ -135,7 +135,7 @@ class FullscreenTopOverlay extends StatelessWidget { tooltip: isFavourite ? 'Remove from favourites' : 'Add to favourites', ), Sweeper( - builder: (context) => Icon(AIcons.favourite, color: Colors.redAccent), + builder: (context) => const Icon(AIcons.favourite, color: Colors.redAccent), toggledNotifier: entry.isFavouriteNotifier, ), ], diff --git a/lib/widgets/fullscreen/overlay/video.dart b/lib/widgets/fullscreen/overlay/video.dart index 047fa959c..ef34e159e 100644 --- a/lib/widgets/fullscreen/overlay/video.dart +++ b/lib/widgets/fullscreen/overlay/video.dart @@ -124,7 +124,7 @@ class VideoControlOverlayState extends State with SingleTic OverlayButton( scale: scale, child: IconButton( - icon: Icon(OMIcons.openInNew), + icon: const Icon(OMIcons.openInNew), onPressed: () => AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype), tooltip: 'Open', ), diff --git a/lib/widgets/stats/filter_table.dart b/lib/widgets/stats/filter_table.dart new file mode 100644 index 000000000..4eb9a60e3 --- /dev/null +++ b/lib/widgets/stats/filter_table.dart @@ -0,0 +1,94 @@ +import 'package:aves/model/collection_lens.dart'; +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/utils/color_utils.dart'; +import 'package:aves/widgets/album/collection_page.dart'; +import 'package:aves/widgets/common/aves_filter_chip.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:percent_indicator/linear_percent_indicator.dart'; + +class FilterTable extends StatelessWidget { + final CollectionLens collection; + final Map entryCountMap; + final CollectionFilter Function(String key) filterBuilder; + + const FilterTable({ + @required this.collection, + @required this.entryCountMap, + @required this.filterBuilder, + }); + + static const chipWidth = AvesFilterChip.maxChipWidth; + static const countWidth = 32.0; + static const percentIndicatorMinWidth = 80.0; + + @override + Widget build(BuildContext context) { + 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 EdgeInsetsDirectional.only(start: AvesFilterChip.buttonBorderWidth / 2 + 6, end: 8), + child: LayoutBuilder( + builder: (context, constraints) { + final showPercentIndicator = constraints.maxWidth - (chipWidth + countWidth) > percentIndicatorMinWidth; + return Table( + children: sortedEntries.take(5).map((kv) { + final filter = filterBuilder(kv.key); + final label = filter.label; + final count = kv.value; + final percent = count / maxCount; + return TableRow( + children: [ + Align( + alignment: AlignmentDirectional.centerStart, + child: AvesFilterChip( + filter: filter, + onPressed: (filter) => _goToCollection(context, filter), + ), + ), + if (showPercentIndicator) + 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: MaxColumnWidth(IntrinsicColumnWidth(), FixedColumnWidth(chipWidth)), + 2: MaxColumnWidth(IntrinsicColumnWidth(), FixedColumnWidth(countWidth)), + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + ); + }, + ), + ); + } + + void _goToCollection(BuildContext context, CollectionFilter filter) { + if (collection == null) return; + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => CollectionPage(collection.derive(filter)), + ), + (route) => false, + ); + } +} diff --git a/lib/widgets/stats.dart b/lib/widgets/stats/stats.dart similarity index 73% rename from lib/widgets/stats.dart rename to lib/widgets/stats/stats.dart index 3dd4c7e8d..1aca8809c 100644 --- a/lib/widgets/stats.dart +++ b/lib/widgets/stats/stats.dart @@ -7,11 +7,10 @@ import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/image_entry.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/album/empty.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/icons.dart'; +import 'package:aves/widgets/stats/filter_table.dart'; import 'package:charts_flutter/flutter.dart' as charts; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -87,9 +86,9 @@ class StatsPage extends StatelessWidget { ], ), ), - ..._buildTopFilters(context, 'Top countries', entryCountPerCountry, (s) => LocationFilter(LocationLevel.country, s)), - ..._buildTopFilters(context, 'Top places', entryCountPerPlace, (s) => LocationFilter(LocationLevel.place, s)), - ..._buildTopFilters(context, 'Top tags', entryCountPerTag, (s) => TagFilter(s)), + ..._buildTopFilters('Top Countries', entryCountPerCountry, (s) => LocationFilter(LocationLevel.country, s)), + ..._buildTopFilters('Top Places', entryCountPerPlace, (s) => LocationFilter(LocationLevel.place, s)), + ..._buildTopFilters('Top Tags', entryCountPerTag, (s) => TagFilter(s)), ], ); } @@ -105,12 +104,17 @@ class StatsPage extends StatelessWidget { ); } + String _cleanMime(String mime) { + mime = mime.toUpperCase().replaceFirst(RegExp('.*/(X-)?'), '').replaceFirst('+XML', ''); + return mime; + } + Widget _buildMimeDonut(BuildContext context, String Function(num) label, Map byMimeTypes) { if (byMimeTypes.isEmpty) return const SizedBox.shrink(); 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(_cleanMime(kv.key), kv.value)).toList(); seriesData.sort((kv1, kv2) { final c = kv2.value.compareTo(kv1.value); return c != 0 ? c : compareAsciiUpperCase(kv1.key, kv2.key); @@ -195,19 +199,12 @@ class StatsPage extends StatelessWidget { } List _buildTopFilters( - BuildContext context, String title, Map entryCountMap, CollectionFilter Function(String key) 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), @@ -216,60 +213,13 @@ class StatsPage extends StatelessWidget { style: Constants.titleTextStyle, ), ), - Padding( - padding: const EdgeInsetsDirectional.only(start: AvesFilterChip.buttonBorderWidth / 2 + 6, end: 8), - child: Table( - children: sortedEntries.take(5).map((kv) { - final filter = filterBuilder(kv.key); - final label = filter.label; - final count = kv.value; - final percent = count / maxCount; - return TableRow( - children: [ - Align( - alignment: AlignmentDirectional.centerStart, - child: AvesFilterChip( - filter: filter, - onPressed: (filter) => _goToCollection(context, filter), - ), - ), - 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, - ), + FilterTable( + collection: collection, + entryCountMap: entryCountMap, + filterBuilder: filterBuilder, ), ]; } - - void _goToCollection(BuildContext context, CollectionFilter filter) { - if (collection == null) return; - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute( - builder: (context) => CollectionPage(collection.derive(filter)), - ), - (route) => false, - ); - } } class StringNumDatum {