stats: improved mime legend, filter table layout
This commit is contained in:
parent
45b3284497
commit
aa99129abf
11 changed files with 123 additions and 79 deletions
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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...',
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
|
|
94
lib/widgets/stats/filter_table.dart
Normal file
94
lib/widgets/stats/filter_table.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 {
|
Loading…
Reference in a new issue