minor fixes (app bar progress subtitle, welcome terms, new album dialog, catalog/locating priority)
This commit is contained in:
parent
2b63ae17bc
commit
073de89362
13 changed files with 218 additions and 182 deletions
|
@ -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");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
priority: ServiceCallPriority.getLocation,
|
? servicePolicy.call(
|
||||||
);
|
call,
|
||||||
|
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(
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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});
|
||||||
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -23,33 +23,36 @@ 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)
|
// 'isAnimated': animated gif/webp (bool)
|
||||||
// 'isAnimated': animated gif/webp (bool)
|
// 'latitude': latitude (double)
|
||||||
// 'latitude': latitude (double)
|
// 'longitude': longitude (double)
|
||||||
// 'longitude': longitude (double)
|
// 'videoRotation': video rotation degrees (int)
|
||||||
// 'videoRotation': video rotation degrees (int)
|
// 'xmpSubjects': ';' separated XMP subjects (string)
|
||||||
// 'xmpSubjects': ';' separated XMP subjects (string)
|
// 'xmpTitleDescription': XMP title or XMP description (string)
|
||||||
// 'xmpTitleDescription': XMP title or XMP description (string)
|
final result = await platform.invokeMethod('getCatalogMetadata', <String, dynamic>{
|
||||||
final result = await platform.invokeMethod('getCatalogMetadata', <String, dynamic>{
|
'mimeType': entry.mimeType,
|
||||||
'mimeType': entry.mimeType,
|
'uri': entry.uri,
|
||||||
'uri': entry.uri,
|
}) as Map;
|
||||||
}) as Map;
|
result['contentId'] = entry.contentId;
|
||||||
result['contentId'] = entry.contentId;
|
return CatalogMetadata.fromMap(result);
|
||||||
return CatalogMetadata.fromMap(result);
|
} on PlatformException catch (e) {
|
||||||
} on PlatformException catch (e) {
|
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
|
||||||
priority: ServiceCallPriority.getMetadata,
|
? servicePolicy.call(
|
||||||
);
|
call,
|
||||||
|
priority: ServiceCallPriority.getMetadata,
|
||||||
|
)
|
||||||
|
: call();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<OverlayMetadata> getOverlayMetadata(ImageEntry entry) async {
|
static Future<OverlayMetadata> getOverlayMetadata(ImageEntry entry) async {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
source: collection.source,
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -38,36 +38,38 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
if (allVolumes.length > 1) ...[
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text('Storage:'),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: DropdownButton<StorageVolume>(
|
||||||
|
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(
|
TextField(
|
||||||
controller: _nameController,
|
controller: _nameController,
|
||||||
// autofocus: true,
|
// autofocus: true,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Text('Storage:'),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: DropdownButton<StorageVolume>(
|
|
||||||
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),
|
contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 0),
|
||||||
|
|
93
lib/widgets/common/app_bar_subtitle.dart
Normal file
93
lib/widgets/common/app_bar_subtitle.dart
Normal 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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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: Text(title),
|
title: SourceStateAwareAppBarTitle(
|
||||||
|
title: Text(title),
|
||||||
|
source: source,
|
||||||
|
),
|
||||||
floating: true,
|
floating: true,
|
||||||
),
|
),
|
||||||
filterEntries: filterEntries,
|
filterEntries: filterEntries,
|
||||||
|
|
Loading…
Reference in a new issue