stats: improved mime legend, filter table layout

This commit is contained in:
Thibault Deckers 2020-04-21 10:45:10 +09:00
parent 45b3284497
commit aa99129abf
11 changed files with 123 additions and 79 deletions

View file

@ -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,

View file

@ -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<CollectionAppBar> 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<CollectionAppBar> with SingleTickerPr
),
const PopupMenuDivider(),
],
PopupMenuItem(
const PopupMenuItem(
value: CollectionAction.stats,
child: MenuRow(text: 'Stats', icon: OMIcons.pieChart),
),

View file

@ -40,7 +40,7 @@ class ImageSearchDelegate extends SearchDelegate<CollectionFilter> {
if (query.isNotEmpty)
IconButton(
tooltip: 'Clear',
icon: Icon(OMIcons.clear),
icon: const Icon(OMIcons.clear),
onPressed: () {
query = '';
showSuggestions(context);

View file

@ -49,7 +49,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
@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,

View file

@ -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),
],

View file

@ -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),
),
),

View file

@ -211,17 +211,17 @@ class ImageMapState extends State<ImageMap> 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...',
),

View file

@ -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,
),
],

View file

@ -124,7 +124,7 @@ class VideoControlOverlayState extends State<VideoControlOverlay> 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',
),

View file

@ -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<String, int> 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,
);
}
}

View file

@ -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<String, num> 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<Widget> _buildTopFilters(
BuildContext context,
String title,
Map<String, int> 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 {