minor fixes (app bar progress subtitle, welcome terms, new album dialog, catalog/locating priority)

This commit is contained in:
Thibault Deckers 2020-06-20 10:45:18 +09:00
parent 2b63ae17bc
commit 073de89362
13 changed files with 218 additions and 182 deletions

View file

@ -13,25 +13,26 @@ public class Constants {
public static final Map<Integer, String> MEDIA_METADATA_KEYS = new HashMap<Integer, String>() { public static final Map<Integer, String> MEDIA_METADATA_KEYS = new HashMap<Integer, String>() {
{ {
put(MediaMetadataRetriever.METADATA_KEY_MIMETYPE, "MIME Type"); put(MediaMetadataRetriever.METADATA_KEY_ALBUM, "Album");
put(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS, "Number of Tracks"); 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_AUDIO, "Has Audio");
put(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO, "Has Video"); 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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
put(MediaMetadataRetriever.METADATA_KEY_VIDEO_FRAME_COUNT, "Frame Count"); put(MediaMetadataRetriever.METADATA_KEY_VIDEO_FRAME_COUNT, "Frame Count");
} }
put(MediaMetadataRetriever.METADATA_KEY_DATE, "Date"); // TODO TLAD comment? category?
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");
} }
}; };
} }

View file

@ -13,4 +13,5 @@ __We collect anonymous data to improve the app.__ We use Google Firebase for Ana
## Links ## Links
[Sources](https://github.com/deckerst/aves) [Sources](https://github.com/deckerst/aves)
[License](https://github.com/deckerst/aves/blob/master/LICENSE) [License](https://github.com/deckerst/aves/blob/master/LICENSE)

View file

@ -242,9 +242,9 @@ class ImageEntry {
addressDetails = null; addressDetails = null;
} }
Future<void> catalog() async { Future<void> catalog({bool background = false}) async {
if (isCatalogued) return; if (isCatalogued) return;
catalogMetadata = await MetadataService.getCatalogMetadata(this); catalogMetadata = await MetadataService.getCatalogMetadata(this, background: background);
} }
AddressDetails get addressDetails => _addressDetails; AddressDetails get addressDetails => _addressDetails;
@ -254,20 +254,23 @@ class ImageEntry {
addressChangeNotifier.notifyListeners(); addressChangeNotifier.notifyListeners();
} }
Future<void> locate() async { Future<void> locate({bool background = false}) async {
if (isLocated) return; if (isLocated) return;
await catalog(); await catalog(background: background);
final latitude = _catalogMetadata?.latitude; final latitude = _catalogMetadata?.latitude;
final longitude = _catalogMetadata?.longitude; final longitude = _catalogMetadata?.longitude;
if (latitude == null || longitude == null) return; if (latitude == null || longitude == null) return;
final coordinates = Coordinates(latitude, longitude); final coordinates = Coordinates(latitude, longitude);
try { try {
final addresses = await servicePolicy.call( final call = () => Geocoder.local.findAddressesFromCoordinates(coordinates);
() => Geocoder.local.findAddressesFromCoordinates(coordinates), final addresses = await (background
? servicePolicy.call(
call,
priority: ServiceCallPriority.getLocation, priority: ServiceCallPriority.getLocation,
); )
: call());
if (addresses != null && addresses.isNotEmpty) { if (addresses != null && addresses.isNotEmpty) {
final address = addresses.first; final address = addresses.first;
addressDetails = AddressDetails( addressDetails = AddressDetails(

View file

@ -134,7 +134,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
switch (sortFactor) { switch (sortFactor) {
case SortFactor.date: case SortFactor.date:
_filteredEntries.sort((a, b) { _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); return c != 0 ? c : compareAsciiUpperCase(a.bestTitle, b.bestTitle);
}); });
break; break;

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/model/image_metadata.dart'; import 'package:aves/model/image_metadata.dart';
@ -23,6 +25,12 @@ mixin SourceBase {
final Map<CollectionFilter, int> _filterEntryCountMap = {}; final Map<CollectionFilter, int> _filterEntryCountMap = {};
void invalidateFilterEntryCounts() => _filterEntryCountMap.clear(); void invalidateFilterEntryCounts() => _filterEntryCountMap.clear();
final StreamController<ProgressEvent> _progressStreamController = StreamController.broadcast();
Stream<ProgressEvent> 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 { class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin {
@ -118,3 +126,9 @@ class EntryMovedEvent {
const EntryMovedEvent(this.entries); const EntryMovedEvent(this.entries);
} }
class ProgressEvent {
final int done, total;
const ProgressEvent({@required this.done, @required this.total});
}

View file

@ -24,12 +24,16 @@ mixin LocationMixin on SourceBase {
Future<void> locateEntries() async { Future<void> locateEntries() async {
// final stopwatch = Stopwatch()..start(); // final stopwatch = Stopwatch()..start();
final unlocatedEntries = rawEntries.where((entry) => entry.hasGps && !entry.isLocated).toList(); final todo = rawEntries.where((entry) => entry.hasGps && !entry.isLocated).toList();
if (unlocatedEntries.isEmpty) return; if (todo.isEmpty) return;
var progressDone = 0;
final progressTotal = todo.length;
setProgress(done: progressDone, total: progressTotal);
final newAddresses = <AddressDetails>[]; final newAddresses = <AddressDetails>[];
await Future.forEach<ImageEntry>(unlocatedEntries, (entry) async { await Future.forEach<ImageEntry>(todo, (entry) async {
await entry.locate(); await entry.locate(background: true);
if (entry.isLocated) { if (entry.isLocated) {
newAddresses.add(entry.addressDetails); newAddresses.add(entry.addressDetails);
if (newAddresses.length >= _commitCountThreshold) { if (newAddresses.length >= _commitCountThreshold) {
@ -37,6 +41,7 @@ mixin LocationMixin on SourceBase {
newAddresses.clear(); newAddresses.clear();
} }
} }
setProgress(done: ++progressDone, total: progressTotal);
}); });
await metadataDb.saveAddresses(List.unmodifiable(newAddresses)); await metadataDb.saveAddresses(List.unmodifiable(newAddresses));
onAddressMetadataChanged(); onAddressMetadataChanged();

View file

@ -23,12 +23,16 @@ mixin TagMixin on SourceBase {
Future<void> catalogEntries() async { Future<void> catalogEntries() async {
// final stopwatch = Stopwatch()..start(); // final stopwatch = Stopwatch()..start();
final uncataloguedEntries = rawEntries.where((entry) => !entry.isCatalogued).toList(); final todo = rawEntries.where((entry) => !entry.isCatalogued).toList();
if (uncataloguedEntries.isEmpty) return; if (todo.isEmpty) return;
var progressDone = 0;
final progressTotal = todo.length;
setProgress(done: progressDone, total: progressTotal);
final newMetadata = <CatalogMetadata>[]; final newMetadata = <CatalogMetadata>[];
await Future.forEach<ImageEntry>(uncataloguedEntries, (entry) async { await Future.forEach<ImageEntry>(todo, (entry) async {
await entry.catalog(); await entry.catalog(background: true);
if (entry.isCatalogued) { if (entry.isCatalogued) {
newMetadata.add(entry.catalogMetadata); newMetadata.add(entry.catalogMetadata);
if (newMetadata.length >= _commitCountThreshold) { if (newMetadata.length >= _commitCountThreshold) {
@ -36,6 +40,7 @@ mixin TagMixin on SourceBase {
newMetadata.clear(); newMetadata.clear();
} }
} }
setProgress(done: ++progressDone, total: progressTotal);
}); });
await metadataDb.saveMetadata(List.unmodifiable(newMetadata)); await metadataDb.saveMetadata(List.unmodifiable(newMetadata));
onCatalogMetadataChanged(); onCatalogMetadataChanged();

View file

@ -23,11 +23,10 @@ class MetadataService {
return {}; return {};
} }
static Future<CatalogMetadata> getCatalogMetadata(ImageEntry entry) async { static Future<CatalogMetadata> getCatalogMetadata(ImageEntry entry, {bool background = false}) async {
if (entry.isSvg) return null; if (entry.isSvg) return null;
return servicePolicy.call( final call = () async {
() async {
try { try {
// return map with: // return map with:
// 'dateMillis': date taken in milliseconds since Epoch (long) // 'dateMillis': date taken in milliseconds since Epoch (long)
@ -47,9 +46,13 @@ class MetadataService {
debugPrint('getCatalogMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}'); debugPrint('getCatalogMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}');
} }
return null; return null;
}, };
return background
? servicePolicy.call(
call,
priority: ServiceCallPriority.getMetadata, priority: ServiceCallPriority.getMetadata,
); )
: call();
} }
static Future<OverlayMetadata> getOverlayMetadata(ImageEntry entry) async { static Future<OverlayMetadata> getOverlayMetadata(ImageEntry entry) async {

View file

@ -29,7 +29,6 @@ class Durations {
static const opToastDisplay = Duration(seconds: 2); static const opToastDisplay = Duration(seconds: 2);
static const collectionScrollMonitoringTimerDelay = Duration(milliseconds: 100); static const collectionScrollMonitoringTimerDelay = Duration(milliseconds: 100);
static const collectionScalingCompleteNotificationDelay = Duration(milliseconds: 300); static const collectionScalingCompleteNotificationDelay = Duration(milliseconds: 300);
static const appBarProgressTimerInterval = Duration(seconds: 1);
static const videoProgressTimerInterval = Duration(milliseconds: 300); static const videoProgressTimerInterval = Duration(milliseconds: 300);
static var staggeredAnimationDelay = Durations.staggeredAnimation ~/ 6 * timeDilation; static var staggeredAnimationDelay = Durations.staggeredAnimation ~/ 6 * timeDilation;
} }

View file

@ -1,14 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/main.dart'; import 'package:aves/main.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/model/settings.dart'; import 'package:aves/model/settings.dart';
import 'package:aves/model/source/collection_lens.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/utils/durations.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/action_delegates/selection_action_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/data_providers/media_store_collection_provider.dart';
import 'package:aves/widgets/common/entry_actions.dart'; import 'package:aves/widgets/common/entry_actions.dart';
import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/icons.dart';
@ -134,32 +133,9 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
if (collection.isBrowsing) { if (collection.isBrowsing) {
Widget title = Text(AvesApp.mode == AppMode.pick ? 'Select' : 'Aves'); Widget title = Text(AvesApp.mode == AppMode.pick ? 'Select' : 'Aves');
if (AvesApp.mode == AppMode.main) { if (AvesApp.mode == AppMode.main) {
title = Column( title = SourceStateAwareAppBarTitle(
mainAxisSize: MainAxisSize.min, title: title,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title,
ValueListenableBuilder<SourceState>(
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, source: collection.source,
),
);
},
),
],
); );
} }
return GestureDetector( return GestureDetector(
@ -403,73 +379,3 @@ enum CollectionAction {
sortBySize, sortBySize,
sortByName, sortByName,
} }
class SourceStateSubtitle extends StatefulWidget {
final CollectionSource source;
const SourceStateSubtitle({@required this.source});
@override
_SourceStateSubtitleState createState() => _SourceStateSubtitleState();
}
class _SourceStateSubtitleState extends State<SourceStateSubtitle> {
Timer _progressTimer;
CollectionSource get source => widget.source;
SourceState get sourceState => source.stateNotifier.value;
List<ImageEntry> 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),
),
]
],
);
}
}

View file

@ -38,11 +38,7 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
TextField( if (allVolumes.length > 1) ...[
controller: _nameController,
// autofocus: true,
),
const SizedBox(height: 16),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -68,6 +64,12 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
), ),
], ],
), ),
const SizedBox(height: 16),
],
TextField(
controller: _nameController,
// autofocus: true,
),
], ],
), ),
contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 0), contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 0),

View file

@ -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<SourceState>(
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<ProgressEvent>(
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),
),
);
},
),
],
);
}
}

View file

@ -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/raster.dart';
import 'package:aves/widgets/album/thumbnail/vector.dart'; import 'package:aves/widgets/album/thumbnail/vector.dart';
import 'package:aves/widgets/app_drawer.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/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';
@ -39,7 +40,10 @@ class FilterNavigationPage extends StatelessWidget {
return FilterGridPage( return FilterGridPage(
source: source, source: source,
appBar: SliverAppBar( appBar: SliverAppBar(
title: SourceStateAwareAppBarTitle(
title: Text(title), title: Text(title),
source: source,
),
floating: true, floating: true,
), ),
filterEntries: filterEntries, filterEntries: filterEntries,