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,
|
brightness: Brightness.dark,
|
||||||
accentColor: Colors.indigoAccent,
|
accentColor: Colors.indigoAccent,
|
||||||
scaffoldBackgroundColor: Colors.grey[900],
|
scaffoldBackgroundColor: Colors.grey[900],
|
||||||
appBarTheme: AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
textTheme: TextTheme(
|
textTheme: TextTheme(
|
||||||
headline6: TextStyle(
|
headline6: TextStyle(
|
||||||
fontSize: 20,
|
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/filter_bar.dart';
|
||||||
import 'package:aves/widgets/album/search/search_delegate.dart';
|
import 'package:aves/widgets/album/search/search_delegate.dart';
|
||||||
import 'package:aves/widgets/common/menu_row.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/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:outline_material_icons/outline_material_icons.dart';
|
import 'package:outline_material_icons/outline_material_icons.dart';
|
||||||
|
@ -149,12 +149,12 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
switch (stateNotifier.value) {
|
switch (stateNotifier.value) {
|
||||||
case PageState.browse:
|
case PageState.browse:
|
||||||
return IconButton(
|
return IconButton(
|
||||||
icon: Icon(OMIcons.search),
|
icon: const Icon(OMIcons.search),
|
||||||
onPressed: _goToSearch,
|
onPressed: _goToSearch,
|
||||||
);
|
);
|
||||||
case PageState.search:
|
case PageState.search:
|
||||||
return IconButton(
|
return IconButton(
|
||||||
icon: Icon(OMIcons.clear),
|
icon: const Icon(OMIcons.clear),
|
||||||
onPressed: () => _searchFieldController.clear(),
|
onPressed: () => _searchFieldController.clear(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -192,7 +192,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
),
|
),
|
||||||
const PopupMenuDivider(),
|
const PopupMenuDivider(),
|
||||||
],
|
],
|
||||||
PopupMenuItem(
|
const PopupMenuItem(
|
||||||
value: CollectionAction.stats,
|
value: CollectionAction.stats,
|
||||||
child: MenuRow(text: 'Stats', icon: OMIcons.pieChart),
|
child: MenuRow(text: 'Stats', icon: OMIcons.pieChart),
|
||||||
),
|
),
|
||||||
|
|
|
@ -40,7 +40,7 @@ class ImageSearchDelegate extends SearchDelegate<CollectionFilter> {
|
||||||
if (query.isNotEmpty)
|
if (query.isNotEmpty)
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Clear',
|
tooltip: 'Clear',
|
||||||
icon: Icon(OMIcons.clear),
|
icon: const Icon(OMIcons.clear),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
query = '';
|
query = '';
|
||||||
showSuggestions(context);
|
showSuggestions(context);
|
||||||
|
|
|
@ -49,7 +49,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final leading = filter.iconBuilder(context, AvesFilterChip.iconSize);
|
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(
|
final child = Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|
|
@ -20,7 +20,7 @@ class MenuRow extends StatelessWidget {
|
||||||
if (checked != null) ...[
|
if (checked != null) ...[
|
||||||
Opacity(
|
Opacity(
|
||||||
opacity: checked ? 1 : 0,
|
opacity: checked ? 1 : 0,
|
||||||
child: Icon(OMIcons.done),
|
child: const Icon(OMIcons.done),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
],
|
],
|
||||||
|
|
|
@ -10,9 +10,9 @@ ScrollThumbBuilder avesScrollThumbBuilder({
|
||||||
@required Color backgroundColor,
|
@required Color backgroundColor,
|
||||||
}) {
|
}) {
|
||||||
final scrollThumb = Container(
|
final scrollThumb = Container(
|
||||||
decoration: BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.black26,
|
color: Colors.black26,
|
||||||
borderRadius: const BorderRadius.all(
|
borderRadius: BorderRadius.all(
|
||||||
Radius.circular(12.0),
|
Radius.circular(12.0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -211,17 +211,17 @@ class ImageMapState extends State<ImageMap> with AutomaticKeepAliveClientMixin {
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Column(children: [
|
Column(children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(OMIcons.add),
|
icon: const Icon(OMIcons.add),
|
||||||
onPressed: _controller == null ? null : () => _zoomBy(1),
|
onPressed: _controller == null ? null : () => _zoomBy(1),
|
||||||
tooltip: 'Zoom in',
|
tooltip: 'Zoom in',
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(OMIcons.remove),
|
icon: const Icon(OMIcons.remove),
|
||||||
onPressed: _controller == null ? null : () => _zoomBy(-1),
|
onPressed: _controller == null ? null : () => _zoomBy(-1),
|
||||||
tooltip: 'Zoom out',
|
tooltip: 'Zoom out',
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(OMIcons.openInNew),
|
icon: const Icon(OMIcons.openInNew),
|
||||||
onPressed: () => AndroidAppService.openMap(widget.geoUri),
|
onPressed: () => AndroidAppService.openMap(widget.geoUri),
|
||||||
tooltip: 'Show on map...',
|
tooltip: 'Show on map...',
|
||||||
),
|
),
|
||||||
|
|
|
@ -135,7 +135,7 @@ class FullscreenTopOverlay extends StatelessWidget {
|
||||||
tooltip: isFavourite ? 'Remove from favourites' : 'Add to favourites',
|
tooltip: isFavourite ? 'Remove from favourites' : 'Add to favourites',
|
||||||
),
|
),
|
||||||
Sweeper(
|
Sweeper(
|
||||||
builder: (context) => Icon(AIcons.favourite, color: Colors.redAccent),
|
builder: (context) => const Icon(AIcons.favourite, color: Colors.redAccent),
|
||||||
toggledNotifier: entry.isFavouriteNotifier,
|
toggledNotifier: entry.isFavouriteNotifier,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -124,7 +124,7 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
|
||||||
OverlayButton(
|
OverlayButton(
|
||||||
scale: scale,
|
scale: scale,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(OMIcons.openInNew),
|
icon: const Icon(OMIcons.openInNew),
|
||||||
onPressed: () => AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype),
|
onPressed: () => AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype),
|
||||||
tooltip: 'Open',
|
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/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/utils/constants.dart';
|
||||||
import 'package:aves/widgets/album/collection_page.dart';
|
|
||||||
import 'package:aves/widgets/album/empty.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/data_providers/media_query_data_provider.dart';
|
||||||
import 'package:aves/widgets/common/icons.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:charts_flutter/flutter.dart' as charts;
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.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('Top Countries', entryCountPerCountry, (s) => LocationFilter(LocationLevel.country, s)),
|
||||||
..._buildTopFilters(context, 'Top places', entryCountPerPlace, (s) => LocationFilter(LocationLevel.place, s)),
|
..._buildTopFilters('Top Places', entryCountPerPlace, (s) => LocationFilter(LocationLevel.place, s)),
|
||||||
..._buildTopFilters(context, 'Top tags', entryCountPerTag, (s) => TagFilter(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) {
|
Widget _buildMimeDonut(BuildContext context, String Function(num) label, Map<String, num> byMimeTypes) {
|
||||||
if (byMimeTypes.isEmpty) return const SizedBox.shrink();
|
if (byMimeTypes.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
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(_cleanMime(kv.key), kv.value)).toList();
|
||||||
seriesData.sort((kv1, kv2) {
|
seriesData.sort((kv1, kv2) {
|
||||||
final c = kv2.value.compareTo(kv1.value);
|
final c = kv2.value.compareTo(kv1.value);
|
||||||
return c != 0 ? c : compareAsciiUpperCase(kv1.key, kv2.key);
|
return c != 0 ? c : compareAsciiUpperCase(kv1.key, kv2.key);
|
||||||
|
@ -195,19 +199,12 @@ class StatsPage extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildTopFilters(
|
List<Widget> _buildTopFilters(
|
||||||
BuildContext context,
|
|
||||||
String title,
|
String title,
|
||||||
Map<String, int> entryCountMap,
|
Map<String, int> entryCountMap,
|
||||||
CollectionFilter Function(String key) filterBuilder,
|
CollectionFilter Function(String key) filterBuilder,
|
||||||
) {
|
) {
|
||||||
if (entryCountMap.isEmpty) return [];
|
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 [
|
return [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
|
@ -216,60 +213,13 @@ class StatsPage extends StatelessWidget {
|
||||||
style: Constants.titleTextStyle,
|
style: Constants.titleTextStyle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
FilterTable(
|
||||||
padding: const EdgeInsetsDirectional.only(start: AvesFilterChip.buttonBorderWidth / 2 + 6, end: 8),
|
collection: collection,
|
||||||
child: Table(
|
entryCountMap: entryCountMap,
|
||||||
children: sortedEntries.take(5).map((kv) {
|
filterBuilder: filterBuilder,
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
void _goToCollection(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