From 073de893620cdf5bbb853de40a1fd888fc745797 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sat, 20 Jun 2020 10:45:18 +0900 Subject: [PATCH] minor fixes (app bar progress subtitle, welcome terms, new album dialog, catalog/locating priority) --- .../thibault/aves/utils/Constants.java | 29 ++--- assets/terms.md | 1 + lib/model/image_entry.dart | 19 ++-- lib/model/source/collection_lens.dart | 2 +- lib/model/source/collection_source.dart | 14 +++ lib/model/source/location.dart | 13 ++- lib/model/source/tag.dart | 13 ++- lib/services/metadata_service.dart | 53 ++++----- lib/utils/durations.dart | 1 - lib/widgets/album/app_bar.dart | 102 +----------------- .../action_delegates/create_album_dialog.dart | 54 +++++----- lib/widgets/common/app_bar_subtitle.dart | 93 ++++++++++++++++ lib/widgets/filter_grid_page.dart | 6 +- 13 files changed, 218 insertions(+), 182 deletions(-) create mode 100644 lib/widgets/common/app_bar_subtitle.dart diff --git a/android/app/src/main/java/deckers/thibault/aves/utils/Constants.java b/android/app/src/main/java/deckers/thibault/aves/utils/Constants.java index e88b50774..f99926817 100644 --- a/android/app/src/main/java/deckers/thibault/aves/utils/Constants.java +++ b/android/app/src/main/java/deckers/thibault/aves/utils/Constants.java @@ -13,25 +13,26 @@ public class Constants { public static final Map MEDIA_METADATA_KEYS = new HashMap() { { - put(MediaMetadataRetriever.METADATA_KEY_MIMETYPE, "MIME Type"); - put(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS, "Number of Tracks"); + put(MediaMetadataRetriever.METADATA_KEY_ALBUM, "Album"); + put(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, "Album Artist"); + put(MediaMetadataRetriever.METADATA_KEY_ARTIST, "Artist"); + put(MediaMetadataRetriever.METADATA_KEY_AUTHOR, "Author"); + put(MediaMetadataRetriever.METADATA_KEY_BITRATE, "Bitrate"); + put(MediaMetadataRetriever.METADATA_KEY_COMPOSER, "Composer"); + put(MediaMetadataRetriever.METADATA_KEY_DATE, "Date"); + put(MediaMetadataRetriever.METADATA_KEY_GENRE, "Content Type"); put(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO, "Has Audio"); put(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO, "Has Video"); - put(MediaMetadataRetriever.METADATA_KEY_BITRATE, "Bitrate"); + put(MediaMetadataRetriever.METADATA_KEY_LOCATION, "Location"); + put(MediaMetadataRetriever.METADATA_KEY_MIMETYPE, "MIME Type"); + put(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS, "Number of Tracks"); + put(MediaMetadataRetriever.METADATA_KEY_TITLE, "Title"); + put(MediaMetadataRetriever.METADATA_KEY_WRITER, "Writer"); + put(MediaMetadataRetriever.METADATA_KEY_YEAR, "Year"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { put(MediaMetadataRetriever.METADATA_KEY_VIDEO_FRAME_COUNT, "Frame Count"); } - put(MediaMetadataRetriever.METADATA_KEY_DATE, "Date"); - put(MediaMetadataRetriever.METADATA_KEY_LOCATION, "Location"); - put(MediaMetadataRetriever.METADATA_KEY_YEAR, "Year"); - put(MediaMetadataRetriever.METADATA_KEY_ARTIST, "Artist"); - put(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, "Album Artist"); - put(MediaMetadataRetriever.METADATA_KEY_ALBUM, "Album"); - put(MediaMetadataRetriever.METADATA_KEY_TITLE, "Title"); - put(MediaMetadataRetriever.METADATA_KEY_AUTHOR, "Author"); - put(MediaMetadataRetriever.METADATA_KEY_COMPOSER, "Composer"); - put(MediaMetadataRetriever.METADATA_KEY_WRITER, "Writer"); - put(MediaMetadataRetriever.METADATA_KEY_GENRE, "Genre"); + // TODO TLAD comment? category? } }; } diff --git a/assets/terms.md b/assets/terms.md index a8fa66439..eb7e5d18e 100644 --- a/assets/terms.md +++ b/assets/terms.md @@ -13,4 +13,5 @@ __We collect anonymous data to improve the app.__ We use Google Firebase for Ana ## Links [Sources](https://github.com/deckerst/aves) + [License](https://github.com/deckerst/aves/blob/master/LICENSE) diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index 7839a7404..51bffc20d 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -242,9 +242,9 @@ class ImageEntry { addressDetails = null; } - Future catalog() async { + Future catalog({bool background = false}) async { if (isCatalogued) return; - catalogMetadata = await MetadataService.getCatalogMetadata(this); + catalogMetadata = await MetadataService.getCatalogMetadata(this, background: background); } AddressDetails get addressDetails => _addressDetails; @@ -254,20 +254,23 @@ class ImageEntry { addressChangeNotifier.notifyListeners(); } - Future locate() async { + Future locate({bool background = false}) async { if (isLocated) return; - await catalog(); + await catalog(background: background); final latitude = _catalogMetadata?.latitude; final longitude = _catalogMetadata?.longitude; if (latitude == null || longitude == null) return; final coordinates = Coordinates(latitude, longitude); try { - final addresses = await servicePolicy.call( - () => Geocoder.local.findAddressesFromCoordinates(coordinates), - priority: ServiceCallPriority.getLocation, - ); + final call = () => Geocoder.local.findAddressesFromCoordinates(coordinates); + final addresses = await (background + ? servicePolicy.call( + call, + priority: ServiceCallPriority.getLocation, + ) + : call()); if (addresses != null && addresses.isNotEmpty) { final address = addresses.first; addressDetails = AddressDetails( diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart index ac62fc887..9fe1b0e55 100644 --- a/lib/model/source/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -134,7 +134,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel switch (sortFactor) { case SortFactor.date: _filteredEntries.sort((a, b) { - final c = b.bestDate.compareTo(a.bestDate); + final c = b.bestDate?.compareTo(a.bestDate) ?? -1; return c != 0 ? c : compareAsciiUpperCase(a.bestTitle, b.bestTitle); }); break; diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index 6687d10f6..a073a87e1 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_metadata.dart'; @@ -23,6 +25,12 @@ mixin SourceBase { final Map _filterEntryCountMap = {}; void invalidateFilterEntryCounts() => _filterEntryCountMap.clear(); + + final StreamController _progressStreamController = StreamController.broadcast(); + + Stream get progressStream => _progressStreamController.stream; + + void setProgress({@required int done, @required int total}) => _progressStreamController.add(ProgressEvent(done: done, total: total)); } class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin { @@ -118,3 +126,9 @@ class EntryMovedEvent { const EntryMovedEvent(this.entries); } + +class ProgressEvent { + final int done, total; + + const ProgressEvent({@required this.done, @required this.total}); +} diff --git a/lib/model/source/location.dart b/lib/model/source/location.dart index b239ef9a2..c2ea53c8d 100644 --- a/lib/model/source/location.dart +++ b/lib/model/source/location.dart @@ -24,12 +24,16 @@ mixin LocationMixin on SourceBase { Future locateEntries() async { // final stopwatch = Stopwatch()..start(); - final unlocatedEntries = rawEntries.where((entry) => entry.hasGps && !entry.isLocated).toList(); - if (unlocatedEntries.isEmpty) return; + final todo = rawEntries.where((entry) => entry.hasGps && !entry.isLocated).toList(); + if (todo.isEmpty) return; + + var progressDone = 0; + final progressTotal = todo.length; + setProgress(done: progressDone, total: progressTotal); final newAddresses = []; - await Future.forEach(unlocatedEntries, (entry) async { - await entry.locate(); + await Future.forEach(todo, (entry) async { + await entry.locate(background: true); if (entry.isLocated) { newAddresses.add(entry.addressDetails); if (newAddresses.length >= _commitCountThreshold) { @@ -37,6 +41,7 @@ mixin LocationMixin on SourceBase { newAddresses.clear(); } } + setProgress(done: ++progressDone, total: progressTotal); }); await metadataDb.saveAddresses(List.unmodifiable(newAddresses)); onAddressMetadataChanged(); diff --git a/lib/model/source/tag.dart b/lib/model/source/tag.dart index c563efcb3..7beb7cf36 100644 --- a/lib/model/source/tag.dart +++ b/lib/model/source/tag.dart @@ -23,12 +23,16 @@ mixin TagMixin on SourceBase { Future catalogEntries() async { // final stopwatch = Stopwatch()..start(); - final uncataloguedEntries = rawEntries.where((entry) => !entry.isCatalogued).toList(); - if (uncataloguedEntries.isEmpty) return; + final todo = rawEntries.where((entry) => !entry.isCatalogued).toList(); + if (todo.isEmpty) return; + + var progressDone = 0; + final progressTotal = todo.length; + setProgress(done: progressDone, total: progressTotal); final newMetadata = []; - await Future.forEach(uncataloguedEntries, (entry) async { - await entry.catalog(); + await Future.forEach(todo, (entry) async { + await entry.catalog(background: true); if (entry.isCatalogued) { newMetadata.add(entry.catalogMetadata); if (newMetadata.length >= _commitCountThreshold) { @@ -36,6 +40,7 @@ mixin TagMixin on SourceBase { newMetadata.clear(); } } + setProgress(done: ++progressDone, total: progressTotal); }); await metadataDb.saveMetadata(List.unmodifiable(newMetadata)); onCatalogMetadataChanged(); diff --git a/lib/services/metadata_service.dart b/lib/services/metadata_service.dart index a3435087a..16cea61ad 100644 --- a/lib/services/metadata_service.dart +++ b/lib/services/metadata_service.dart @@ -23,33 +23,36 @@ class MetadataService { return {}; } - static Future getCatalogMetadata(ImageEntry entry) async { + static Future getCatalogMetadata(ImageEntry entry, {bool background = false}) async { if (entry.isSvg) return null; - return servicePolicy.call( - () async { - try { - // return map with: - // 'dateMillis': date taken in milliseconds since Epoch (long) - // 'isAnimated': animated gif/webp (bool) - // 'latitude': latitude (double) - // 'longitude': longitude (double) - // 'videoRotation': video rotation degrees (int) - // 'xmpSubjects': ';' separated XMP subjects (string) - // 'xmpTitleDescription': XMP title or XMP description (string) - final result = await platform.invokeMethod('getCatalogMetadata', { - 'mimeType': entry.mimeType, - 'uri': entry.uri, - }) as Map; - result['contentId'] = entry.contentId; - return CatalogMetadata.fromMap(result); - } on PlatformException catch (e) { - debugPrint('getCatalogMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}'); - } - return null; - }, - priority: ServiceCallPriority.getMetadata, - ); + final call = () async { + try { + // return map with: + // 'dateMillis': date taken in milliseconds since Epoch (long) + // 'isAnimated': animated gif/webp (bool) + // 'latitude': latitude (double) + // 'longitude': longitude (double) + // 'videoRotation': video rotation degrees (int) + // 'xmpSubjects': ';' separated XMP subjects (string) + // 'xmpTitleDescription': XMP title or XMP description (string) + final result = await platform.invokeMethod('getCatalogMetadata', { + 'mimeType': entry.mimeType, + 'uri': entry.uri, + }) as Map; + result['contentId'] = entry.contentId; + return CatalogMetadata.fromMap(result); + } on PlatformException catch (e) { + debugPrint('getCatalogMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}'); + } + return null; + }; + return background + ? servicePolicy.call( + call, + priority: ServiceCallPriority.getMetadata, + ) + : call(); } static Future getOverlayMetadata(ImageEntry entry) async { diff --git a/lib/utils/durations.dart b/lib/utils/durations.dart index 75b8b4612..9848ebf12 100644 --- a/lib/utils/durations.dart +++ b/lib/utils/durations.dart @@ -29,7 +29,6 @@ class Durations { static const opToastDisplay = Duration(seconds: 2); static const collectionScrollMonitoringTimerDelay = Duration(milliseconds: 100); static const collectionScalingCompleteNotificationDelay = Duration(milliseconds: 300); - static const appBarProgressTimerInterval = Duration(seconds: 1); static const videoProgressTimerInterval = Duration(milliseconds: 300); static var staggeredAnimationDelay = Durations.staggeredAnimation ~/ 6 * timeDilation; } diff --git a/lib/widgets/album/app_bar.dart b/lib/widgets/album/app_bar.dart index 4b732ba4c..41464149d 100644 --- a/lib/widgets/album/app_bar.dart +++ b/lib/widgets/album/app_bar.dart @@ -1,14 +1,13 @@ import 'dart:async'; import 'package:aves/main.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; -import 'package:aves/model/source/collection_source.dart'; import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/album/filter_bar.dart'; import 'package:aves/widgets/album/search/search_delegate.dart'; import 'package:aves/widgets/common/action_delegates/selection_action_delegate.dart'; +import 'package:aves/widgets/common/app_bar_subtitle.dart'; import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart'; import 'package:aves/widgets/common/entry_actions.dart'; import 'package:aves/widgets/common/icons.dart'; @@ -134,32 +133,9 @@ class _CollectionAppBarState extends State with SingleTickerPr if (collection.isBrowsing) { Widget title = Text(AvesApp.mode == AppMode.pick ? 'Select' : 'Aves'); if (AvesApp.mode == AppMode.main) { - title = Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - title, - ValueListenableBuilder( - valueListenable: collection.source.stateNotifier, - builder: (context, sourceState, child) { - return AnimatedSwitcher( - duration: Durations.appBarTitleAnimation, - transitionBuilder: (child, animation) => FadeTransition( - opacity: animation, - child: SizeTransition( - sizeFactor: animation, - child: child, - ), - ), - child: sourceState == SourceState.ready - ? const SizedBox.shrink() - : SourceStateSubtitle( - source: collection.source, - ), - ); - }, - ), - ], + title = SourceStateAwareAppBarTitle( + title: title, + source: collection.source, ); } return GestureDetector( @@ -403,73 +379,3 @@ enum CollectionAction { sortBySize, sortByName, } - -class SourceStateSubtitle extends StatefulWidget { - final CollectionSource source; - - const SourceStateSubtitle({@required this.source}); - - @override - _SourceStateSubtitleState createState() => _SourceStateSubtitleState(); -} - -class _SourceStateSubtitleState extends State { - Timer _progressTimer; - - CollectionSource get source => widget.source; - - SourceState get sourceState => source.stateNotifier.value; - - List get entries => source.rawEntries; - - @override - void initState() { - super.initState(); - _progressTimer = Timer.periodic(Durations.appBarProgressTimerInterval, (_) => setState(() {})); - } - - @override - void dispose() { - _progressTimer.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - String subtitle; - double progress; - switch (sourceState) { - case SourceState.loading: - subtitle = 'Loading'; - break; - case SourceState.cataloguing: - subtitle = 'Cataloguing'; - progress = entries.where((entry) => entry.isCatalogued).length.toDouble() / entries.length; - break; - case SourceState.locating: - subtitle = 'Locating'; - final entriesToLocate = entries.where((entry) => entry.hasGps).toList(); - progress = entriesToLocate.where((entry) => entry.isLocated).length.toDouble() / entriesToLocate.length; - break; - case SourceState.ready: - default: - break; - } - final subtitleStyle = Theme.of(context).textTheme.caption; - return subtitle == null - ? const SizedBox.shrink() - : Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(subtitle, style: subtitleStyle), - if (progress != null && progress > 0) ...[ - const SizedBox(width: 8), - Text( - NumberFormat.percentPattern().format(progress), - style: subtitleStyle.copyWith(color: Colors.white30), - ), - ] - ], - ); - } -} diff --git a/lib/widgets/common/action_delegates/create_album_dialog.dart b/lib/widgets/common/action_delegates/create_album_dialog.dart index b4ecb8ec9..dae918990 100644 --- a/lib/widgets/common/action_delegates/create_album_dialog.dart +++ b/lib/widgets/common/action_delegates/create_album_dialog.dart @@ -38,36 +38,38 @@ class _CreateAlbumDialogState extends State { content: Column( mainAxisSize: MainAxisSize.min, children: [ + if (allVolumes.length > 1) ...[ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Storage:'), + const SizedBox(width: 8), + Expanded( + child: DropdownButton( + isExpanded: true, + items: allVolumes + .map((volume) => DropdownMenuItem( + value: volume, + child: Text( + volume.description, + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ), + )) + .toList(), + value: selectedVolume, + onChanged: (volume) => setState(() => selectedVolume = volume), + ), + ), + ], + ), + const SizedBox(height: 16), + ], TextField( controller: _nameController, // autofocus: true, ), - const SizedBox(height: 16), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Storage:'), - const SizedBox(width: 8), - Expanded( - child: DropdownButton( - isExpanded: true, - items: allVolumes - .map((volume) => DropdownMenuItem( - value: volume, - child: Text( - volume.description, - softWrap: false, - overflow: TextOverflow.fade, - maxLines: 1, - ), - )) - .toList(), - value: selectedVolume, - onChanged: (volume) => setState(() => selectedVolume = volume), - ), - ), - ], - ), ], ), contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 0), diff --git a/lib/widgets/common/app_bar_subtitle.dart b/lib/widgets/common/app_bar_subtitle.dart new file mode 100644 index 000000000..0b1f31ce0 --- /dev/null +++ b/lib/widgets/common/app_bar_subtitle.dart @@ -0,0 +1,93 @@ +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/utils/durations.dart'; +import 'package:flutter/material.dart'; + +class SourceStateAwareAppBarTitle extends StatelessWidget { + final Widget title; + final CollectionSource source; + + const SourceStateAwareAppBarTitle({ + Key key, + @required this.title, + @required this.source, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title, + ValueListenableBuilder( + valueListenable: source.stateNotifier, + builder: (context, sourceState, child) { + return AnimatedSwitcher( + duration: Durations.appBarTitleAnimation, + transitionBuilder: (child, animation) => FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + child: child, + ), + ), + child: sourceState == SourceState.ready + ? const SizedBox.shrink() + : SourceStateSubtitle( + source: source, + ), + ); + }, + ), + ], + ); + } +} + +class SourceStateSubtitle extends StatelessWidget { + final CollectionSource source; + + const SourceStateSubtitle({@required this.source}); + + @override + Widget build(BuildContext context) { + String subtitle; + switch (source.stateNotifier.value) { + case SourceState.loading: + subtitle = 'Loading'; + break; + case SourceState.cataloguing: + subtitle = 'Cataloguing'; + break; + case SourceState.locating: + subtitle = 'Locating'; + break; + case SourceState.ready: + default: + break; + } + final subtitleStyle = Theme.of(context).textTheme.caption; + return subtitle == null + ? const SizedBox.shrink() + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(subtitle, style: subtitleStyle), + StreamBuilder( + stream: source.progressStream, + builder: (context, snapshot) { + if (snapshot.hasError || !snapshot.hasData) return const SizedBox.shrink(); + final progress = snapshot.data; + return Padding( + padding: const EdgeInsetsDirectional.only(start: 8), + child: Text( + '${progress.done}/${progress.total}', + style: subtitleStyle.copyWith(color: Colors.white30), + ), + ); + }, + ), + ], + ); + } +} diff --git a/lib/widgets/filter_grid_page.dart b/lib/widgets/filter_grid_page.dart index 30129ac84..caee6507d 100644 --- a/lib/widgets/filter_grid_page.dart +++ b/lib/widgets/filter_grid_page.dart @@ -12,6 +12,7 @@ import 'package:aves/widgets/album/collection_page.dart'; import 'package:aves/widgets/album/thumbnail/raster.dart'; import 'package:aves/widgets/album/thumbnail/vector.dart'; import 'package:aves/widgets/app_drawer.dart'; +import 'package:aves/widgets/common/app_bar_subtitle.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'; @@ -39,7 +40,10 @@ class FilterNavigationPage extends StatelessWidget { return FilterGridPage( source: source, appBar: SliverAppBar( - title: Text(title), + title: SourceStateAwareAppBarTitle( + title: Text(title), + source: source, + ), floating: true, ), filterEntries: filterEntries,