diff --git a/lib/widgets/stats/stats_page.dart b/lib/widgets/stats/stats_page.dart index a7f6cdb77..0ed592d56 100644 --- a/lib/widgets/stats/stats_page.dart +++ b/lib/widgets/stats/stats_page.dart @@ -13,6 +13,7 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/theme/styles.dart'; +import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/vault_aware.dart'; @@ -58,6 +59,7 @@ class _StatsPageState extends State with FeedbackMixin, VaultAwareMix final Map _entryCountPerTag = {}, _entryCountPerAlbum = {}; final Map _entryCountPerRating = Map.fromEntries(List.generate(7, (i) => MapEntry(5 - i, 0))); late final ValueNotifier _isPageAnimatingNotifier; + int _totalSizeBytes = 0; Set get entries => widget.entries; @@ -72,6 +74,8 @@ class _StatsPageState extends State with FeedbackMixin, VaultAwareMix }); entries.forEach((entry) { + _totalSizeBytes += entry.sizeBytes ?? 0; + if (entry.hasAddress) { final address = entry.addressDetails!; var country = address.countryName; @@ -123,7 +127,6 @@ class _StatsPageState extends State with FeedbackMixin, VaultAwareMix text: l10n.collectionEmptyImages, ); } else { - final theme = Theme.of(context); final chartAnimationDuration = context.read().chartTransition; final byMimeTypes = groupBy(entries, (entry) => entry.mimeType).map((k, v) => MapEntry(k, v.length)); @@ -148,48 +151,21 @@ class _StatsPageState extends State with FeedbackMixin, VaultAwareMix ], ); - final catalogued = entries.where((entry) => entry.isCatalogued); - final withGps = catalogued.where((entry) => entry.hasGps); - final withGpsCount = withGps.length; - final withGpsPercent = withGpsCount / entries.length; - final textScaleFactor = MediaQuery.textScaleFactorOf(context); - final lineHeight = 16 * textScaleFactor; - final barRadius = Radius.circular(lineHeight / 2); - final locationIndicator = Padding( - padding: const EdgeInsets.all(16), - child: Column( + final showRatings = _entryCountPerRating.entries.any((kv) => kv.key != 0 && kv.value > 0); + final source = widget.source; + final sizeIndicator = Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisSize: MainAxisSize.min, children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(AIcons.location), - Expanded( - child: LinearPercentIndicator( - percent: withGpsPercent, - lineHeight: lineHeight, - backgroundColor: theme.colorScheme.onPrimary.withOpacity(.1), - progressColor: theme.colorScheme.secondary, - animation: context.select((v) => v.accessibilityAnimations.animate), - isRTL: context.isRtl, - barRadius: barRadius, - center: LinearPercentIndicatorText(percent: withGpsPercent), - padding: EdgeInsets.symmetric(horizontal: lineHeight), - ), - ), - // end padding to match leading, so that inside label is aligned with outside label below - const SizedBox(width: 24), - ], - ), - const SizedBox(height: 8), - Text( - l10n.statsWithGps(withGpsCount), - textAlign: TextAlign.center, + const Icon(AIcons.size), + const SizedBox(width: 16), + Expanded( + child: Text(formatFileSize(l10n.localeName, _totalSizeBytes)), ), ], ), ); - final showRatings = _entryCountPerRating.entries.any((kv) => kv.key != 0 && kv.value > 0); - final source = widget.source; child = NotificationListener( onNotification: (notification) { _onFilterSelection(context, notification.reversedFilter); @@ -214,7 +190,10 @@ class _StatsPageState extends State with FeedbackMixin, VaultAwareMix animationDuration: chartAnimationDuration, onFilterSelection: (filter) => _onFilterSelection(context, filter), ), - locationIndicator, + const SizedBox(height: 16), + sizeIndicator, + const SizedBox(height: 16), + _LocationIndicator(entries: entries), ..._buildFilterSection(context, l10n.statsTopCountriesSectionTitle, _entryCountPerCountry, (v) => LocationFilter(LocationLevel.country, v)), ..._buildFilterSection(context, l10n.statsTopStatesSectionTitle, _entryCountPerState, (v) => LocationFilter(LocationLevel.state, v)), ..._buildFilterSection(context, l10n.statsTopPlacesSectionTitle, _entryCountPerPlace, (v) => LocationFilter(LocationLevel.place, v)), @@ -418,3 +397,54 @@ class StatsTopPage extends StatelessWidget { ); } } + +class _LocationIndicator extends StatelessWidget { + final Set entries; + + const _LocationIndicator({required this.entries}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final catalogued = entries.where((entry) => entry.isCatalogued); + final withGps = catalogued.where((entry) => entry.hasGps); + final withGpsCount = withGps.length; + final withGpsPercent = withGpsCount / entries.length; + final textScaleFactor = MediaQuery.textScaleFactorOf(context); + final lineHeight = 16 * textScaleFactor; + final barRadius = Radius.circular(lineHeight / 2); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(AIcons.location), + Expanded( + child: LinearPercentIndicator( + percent: withGpsPercent, + lineHeight: lineHeight, + backgroundColor: theme.colorScheme.onPrimary.withOpacity(.1), + progressColor: theme.colorScheme.secondary, + animation: context.select((v) => v.accessibilityAnimations.animate), + isRTL: context.isRtl, + barRadius: barRadius, + center: LinearPercentIndicatorText(percent: withGpsPercent), + padding: EdgeInsets.symmetric(horizontal: lineHeight), + ), + ), + // end padding to match leading, so that inside label is aligned with outside label below + const SizedBox(width: 24), + ], + ), + const SizedBox(height: 8), + Text( + context.l10n.statsWithGps(withGpsCount), + textAlign: TextAlign.center, + ), + ], + ), + ); + } +}