From 48a62e85c535b91cca434886243a7a79c7b60cf9 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 7 Apr 2020 14:50:23 +0900 Subject: [PATCH] misc fixes --- lib/main.dart | 12 +- lib/model/collection_lens.dart | 4 +- lib/model/image_entry.dart | 29 ++-- lib/widgets/album/collection_page.dart | 1 - lib/widgets/album/collection_section.dart | 18 +-- lib/widgets/album/thumbnail_collection.dart | 130 +++++++++--------- .../media_store_collection_provider.dart | 89 +++++------- lib/widgets/common/scroll_thumb.dart | 54 ++++---- .../fullscreen_action_delegate.dart | 4 +- .../fullscreen/info/basic_section.dart | 2 +- lib/widgets/fullscreen/overlay/bottom.dart | 2 +- 11 files changed, 172 insertions(+), 173 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index be7985852..2f59bd02f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,3 @@ -import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_file_service.dart'; import 'package:aves/model/settings.dart'; @@ -12,7 +11,6 @@ import 'package:flutter/services.dart'; import 'package:outline_material_icons/outline_material_icons.dart'; import 'package:pedantic/pedantic.dart'; import 'package:permission_handler/permission_handler.dart'; -import 'package:provider/provider.dart'; import 'package:screen/screen.dart'; void main() { @@ -51,6 +49,7 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { + MediaStoreSource _mediaStore; ImageEntry _sharedEntry; Future _appSetup; @@ -88,6 +87,9 @@ class _HomePageState extends State { // cataloging is essential for geolocation and video rotation await _sharedEntry.catalog(); unawaited(_sharedEntry.locate()); + } else { + _mediaStore = MediaStoreSource(); + unawaited(_mediaStore.fetch()); } } @@ -103,11 +105,7 @@ class _HomePageState extends State { ? SingleFullscreenPage( entry: _sharedEntry, ) - : MediaStoreCollectionProvider( - child: Consumer( - builder: (context, collection, child) => CollectionPage(collection), - ), - ); + : CollectionPage(_mediaStore.collection); }); } } diff --git a/lib/model/collection_lens.dart b/lib/model/collection_lens.dart index 0ef353447..32e9b1e48 100644 --- a/lib/model/collection_lens.dart +++ b/lib/model/collection_lens.dart @@ -128,14 +128,14 @@ class CollectionLens with ChangeNotifier { case SortFactor.date: _filteredEntries.sort((a, b) { final c = b.bestDate.compareTo(a.bestDate); - return c != 0 ? c : compareAsciiUpperCase(a.title, b.title); + return c != 0 ? c : compareAsciiUpperCase(a.bestTitle, b.bestTitle); }); break; case SortFactor.size: _filteredEntries.sort((a, b) => b.sizeBytes.compareTo(a.sizeBytes)); break; case SortFactor.name: - _filteredEntries.sort((a, b) => compareAsciiUpperCase(a.title, b.title)); + _filteredEntries.sort((a, b) => compareAsciiUpperCase(a.bestTitle, b.bestTitle)); break; } } diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index 4b8e5b8bb..85d46f67a 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -130,11 +130,19 @@ class ImageEntry { int get megaPixels => width != null && height != null ? (width * height / 1000000).round() : null; + DateTime _bestDate; + DateTime get bestDate { - if ((catalogMetadata?.dateMillis ?? 0) > 0) return DateTime.fromMillisecondsSinceEpoch(catalogMetadata.dateMillis); - if (sourceDateTakenMillis != null && sourceDateTakenMillis > 0) return DateTime.fromMillisecondsSinceEpoch(sourceDateTakenMillis); - if (dateModifiedSecs != null && dateModifiedSecs > 0) return DateTime.fromMillisecondsSinceEpoch(dateModifiedSecs * 1000); - return null; + if (_bestDate == null) { + if ((catalogMetadata?.dateMillis ?? 0) > 0) { + _bestDate = DateTime.fromMillisecondsSinceEpoch(catalogMetadata.dateMillis); + } else if (sourceDateTakenMillis != null && sourceDateTakenMillis > 0) { + _bestDate = DateTime.fromMillisecondsSinceEpoch(sourceDateTakenMillis); + } else if (dateModifiedSecs != null && dateModifiedSecs > 0) { + _bestDate = DateTime.fromMillisecondsSinceEpoch(dateModifiedSecs * 1000); + } + } + return _bestDate; } DateTime get monthTaken { @@ -159,14 +167,18 @@ class ImageEntry { List get xmpSubjects => catalogMetadata?.xmpSubjects?.split(';')?.where((tag) => tag.isNotEmpty)?.toList() ?? []; - String get title { - if (catalogMetadata != null && catalogMetadata.xmpTitleDescription.isNotEmpty) return catalogMetadata.xmpTitleDescription; - return sourceTitle; + String _bestTitle; + + String get bestTitle { + _bestTitle ??= (catalogMetadata != null && catalogMetadata.xmpTitleDescription.isNotEmpty) ? catalogMetadata.xmpTitleDescription : sourceTitle; + return _bestTitle; } Future catalog() async { if (isCatalogued) return; catalogMetadata = await MetadataService.getCatalogMetadata(this); + _bestDate = null; + _bestTitle = null; if (catalogMetadata != null) { metadataChangeNotifier.notifyListeners(); } @@ -212,7 +224,7 @@ class ImageEntry { } bool search(String query) { - if (title?.toUpperCase()?.contains(query) ?? false) return true; + if (bestTitle?.toUpperCase()?.contains(query) ?? false) return true; if (catalogMetadata?.xmpSubjects?.toUpperCase()?.contains(query) ?? false) return true; if (addressDetails?.addressLine?.toUpperCase()?.contains(query) ?? false) return true; return false; @@ -232,6 +244,7 @@ class ImageEntry { if (contentId is int) this.contentId = contentId; final sourceTitle = newFields['sourceTitle']; if (sourceTitle is String) this.sourceTitle = sourceTitle; + _bestTitle = null; metadataChangeNotifier.notifyListeners(); return true; } diff --git a/lib/widgets/album/collection_page.dart b/lib/widgets/album/collection_page.dart index ed79581c4..d1d254cb9 100644 --- a/lib/widgets/album/collection_page.dart +++ b/lib/widgets/album/collection_page.dart @@ -15,7 +15,6 @@ class CollectionPage extends StatelessWidget { @override Widget build(BuildContext context) { - debugPrint('$runtimeType build'); return MediaQueryDataProvider( child: ChangeNotifierProvider.value( value: collection, diff --git a/lib/widgets/album/collection_section.dart b/lib/widgets/album/collection_section.dart index 2585a08d8..4a24bcd27 100644 --- a/lib/widgets/album/collection_section.dart +++ b/lib/widgets/album/collection_section.dart @@ -86,18 +86,18 @@ class GridThumbnail extends StatelessWidget { return GestureDetector( key: ValueKey(entry.uri), onTap: () => _goToFullscreen(context), - child: Selector( - selector: (c, mq) => mq.size.width, - builder: (c, mqWidth, child) { - return MetaData( - metaData: ThumbnailMetadata(index, entry), - child: Thumbnail( + child: MetaData( + metaData: ThumbnailMetadata(index, entry), + child: Selector( + selector: (c, mq) => mq.size.width, + builder: (c, mqWidth, child) { + return Thumbnail( entry: entry, extent: mqWidth / columnCount, heroTag: collection.heroTag(entry), - ), - ); - }, + ); + }, + ), ), ); } diff --git a/lib/widgets/album/thumbnail_collection.dart b/lib/widgets/album/thumbnail_collection.dart index 3dfa74e4f..06ebeb0bf 100644 --- a/lib/widgets/album/thumbnail_collection.dart +++ b/lib/widgets/album/thumbnail_collection.dart @@ -27,77 +27,81 @@ class ThumbnailCollection extends StatelessWidget { @override Widget build(BuildContext context) { - debugPrint('$runtimeType build'); - final collection = Provider.of(context); - final sections = collection.sections; - final sectionKeys = sections.keys.toList(); - final showHeaders = collection.showHeaders; - return SafeArea( child: Selector( selector: (c, mq) => mq.viewInsets.bottom, builder: (c, mqViewInsetsBottom, child) { - return GridScaleGestureDetector( - scrollableKey: _scrollableKey, - columnCountNotifier: _columnCountNotifier, - child: ValueListenableBuilder( - valueListenable: _columnCountNotifier, - builder: (context, columnCount, child) { - debugPrint('$runtimeType builder columnCount=$columnCount entries=${collection.entryCount}'); - final scrollView = CustomScrollView( - key: _scrollableKey, - primary: true, - // workaround to prevent scrolling the app bar away - // when there is no content and we use `SliverFillRemaining` - physics: collection.isEmpty ? const NeverScrollableScrollPhysics() : null, - slivers: [ - CollectionAppBar( - stateNotifier: stateNotifier, - appBarHeightNotifier: _appBarHeightNotifier, - collection: collection, - ), - if (collection.isEmpty) - SliverFillRemaining( - child: _buildEmptyCollectionPlaceholder(collection), - hasScrollBody: false, - ), - ...sectionKeys.map((sectionKey) => SectionSliver( + return Consumer( + builder: (context, collection, child) { + debugPrint('$runtimeType collection builder entries=${collection.entryCount}'); + final sectionKeys = collection.sections.keys.toList(); + final showHeaders = collection.showHeaders; + return GridScaleGestureDetector( + scrollableKey: _scrollableKey, + columnCountNotifier: _columnCountNotifier, + child: ValueListenableBuilder( + valueListenable: _columnCountNotifier, + builder: (context, columnCount, child) { + debugPrint('$runtimeType columnCount builder entries=${collection.entryCount} columnCount=$columnCount'); + final scrollView = CustomScrollView( + key: _scrollableKey, + primary: true, + // workaround to prevent scrolling the app bar away + // when there is no content and we use `SliverFillRemaining` + physics: collection.isEmpty ? const NeverScrollableScrollPhysics() : null, + slivers: [ + CollectionAppBar( + stateNotifier: stateNotifier, + appBarHeightNotifier: _appBarHeightNotifier, collection: collection, - sectionKey: sectionKey, - columnCount: columnCount, - showHeader: showHeaders, - )), - SliverToBoxAdapter( - child: Selector( - selector: (c, mq) => mq.viewInsets.bottom, - builder: (c, mqViewInsetsBottom, child) { - return SizedBox(height: mqViewInsetsBottom); - }, - ), - ), - ], - ); + ), + if (collection.isEmpty) + SliverFillRemaining( + child: _buildEmptyCollectionPlaceholder(collection), + hasScrollBody: false, + ), + ...sectionKeys.map((sectionKey) => SectionSliver( + collection: collection, + sectionKey: sectionKey, + columnCount: columnCount, + showHeader: showHeaders, + )), + SliverToBoxAdapter( + child: Selector( + selector: (c, mq) => mq.viewInsets.bottom, + builder: (c, mqViewInsetsBottom, child) { + return SizedBox(height: mqViewInsetsBottom); + }, + ), + ), + ], + ); - return ValueListenableBuilder( - valueListenable: _appBarHeightNotifier, - builder: (context, appBarHeight, child) { - return DraggableScrollbar( - heightScrollThumb: avesScrollThumbHeight, - backgroundColor: Colors.white, - scrollThumbBuilder: avesScrollThumbBuilder(), - controller: PrimaryScrollController.of(context), - padding: EdgeInsets.only( - // padding to keep scroll thumb between app bar above and nav bar below - top: appBarHeight, - bottom: mqViewInsetsBottom, - ), - child: child, + return ValueListenableBuilder( + valueListenable: _appBarHeightNotifier, + builder: (context, appBarHeight, child) { + return DraggableScrollbar( + heightScrollThumb: avesScrollThumbHeight, + backgroundColor: Colors.white, + scrollThumbBuilder: avesScrollThumbBuilder( + height: avesScrollThumbHeight, + backgroundColor: Colors.white, + ), + controller: PrimaryScrollController.of(context), + padding: EdgeInsets.only( + // padding to keep scroll thumb between app bar above and nav bar below + top: appBarHeight, + bottom: mqViewInsetsBottom, + ), + child: child, + ); + }, + child: scrollView, ); }, - child: scrollView, - ); - }, - ), + ), + ); + }, ); }, ), diff --git a/lib/widgets/common/data_providers/media_store_collection_provider.dart b/lib/widgets/common/data_providers/media_store_collection_provider.dart index 497b6633b..43211b437 100644 --- a/lib/widgets/common/data_providers/media_store_collection_provider.dart +++ b/lib/widgets/common/data_providers/media_store_collection_provider.dart @@ -8,37 +8,26 @@ import 'package:aves/model/settings.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_native_timezone/flutter_native_timezone.dart'; -import 'package:provider/provider.dart'; -class MediaStoreCollectionProvider extends StatefulWidget { - final Widget child; +class MediaStoreSource { + CollectionSource _source; + CollectionLens _baseLens; - const MediaStoreCollectionProvider({@required this.child}); + CollectionLens get collection => _baseLens; - @override - _MediaStoreCollectionProviderState createState() => _MediaStoreCollectionProviderState(); -} + static const EventChannel _eventChannel = EventChannel('deckers.thibault/aves/mediastore'); -class _MediaStoreCollectionProviderState extends State { - Future collectionFuture; - - static const EventChannel eventChannel = EventChannel('deckers.thibault/aves/mediastore'); - - @override - void initState() { - super.initState(); - collectionFuture = _create(); - } - - Future _create() async { - final stopwatch = Stopwatch()..start(); - final mediaStoreSource = CollectionSource(); - final mediaStoreBaseLens = CollectionLens( - source: mediaStoreSource, + MediaStoreSource() { + _source = CollectionSource(); + _baseLens = CollectionLens( + source: _source, groupFactor: settings.collectionGroupFactor, sortFactor: settings.collectionSortFactor, ); + } + Future fetch() async { + final stopwatch = Stopwatch()..start(); await metadataDb.init(); // <20ms await favourites.init(); final currentTimeZone = await FlutterNativeTimezone.getLocalTimezone(); // <20ms @@ -51,39 +40,31 @@ class _MediaStoreCollectionProviderState extends State[]; - eventChannel.receiveBroadcastStream().cast().listen( - (entryMap) => allEntries.add(ImageEntry.fromMap(entryMap)), - onDone: () async { - debugPrint('$runtimeType stream complete in ${stopwatch.elapsed.inMilliseconds}ms'); - mediaStoreSource.addAll(allEntries); - // TODO reduce setup time until here - mediaStoreSource.updateAlbums(); // <50ms - await mediaStoreSource.loadCatalogMetadata(); // 650ms - await mediaStoreSource.catalogEntries(); // <50ms - await mediaStoreSource.loadAddresses(); // 350ms - await mediaStoreSource.locateEntries(); // <50ms - debugPrint('$runtimeType setup end, elapsed=${stopwatch.elapsed}'); - }, - onError: (error) => debugPrint('$runtimeType mediastore stream error=$error'), - ); + _eventChannel.receiveBroadcastStream().cast().listen( + (entryMap) { + allEntries.add(ImageEntry.fromMap(entryMap)); + if (allEntries.length >= 100) { + _source.addAll(allEntries); + allEntries.clear(); +// debugPrint('$runtimeType streamed ${_source.entries.length} entries at ${stopwatch.elapsed.inMilliseconds}ms'); + } + }, + onDone: () async { + debugPrint('$runtimeType stream complete at ${stopwatch.elapsed.inMilliseconds}ms'); + _source.addAll(allEntries); + // TODO reduce setup time until here + _source.updateAlbums(); // <50ms + await _source.loadCatalogMetadata(); // 650ms + await _source.catalogEntries(); // <50ms + await _source.loadAddresses(); // 350ms + await _source.locateEntries(); // <50ms + debugPrint('$runtimeType setup end, elapsed=${stopwatch.elapsed}'); + }, + onError: (error) => debugPrint('$runtimeType mediastore stream error=$error'), + ); // TODO split image fetch AND/OR cache fetch across sessions + debugPrint('$runtimeType stream start at ${stopwatch.elapsed.inMilliseconds}ms'); await ImageFileService.getImageEntries(); // 460ms - - return mediaStoreBaseLens; - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: collectionFuture, - builder: (futureContext, AsyncSnapshot snapshot) { - final collection = (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) ? snapshot.data : CollectionLens.empty(); - return ChangeNotifierProvider.value( - value: collection, - child: widget.child, - ); - }, - ); } } diff --git a/lib/widgets/common/scroll_thumb.dart b/lib/widgets/common/scroll_thumb.dart index 9f5b3c0b9..5c41312a9 100644 --- a/lib/widgets/common/scroll_thumb.dart +++ b/lib/widgets/common/scroll_thumb.dart @@ -3,7 +3,35 @@ import 'package:flutter/material.dart'; const double avesScrollThumbHeight = 48; -ScrollThumbBuilder avesScrollThumbBuilder() { +// height and background color do not change +// so we do not rely on the builder props +ScrollThumbBuilder avesScrollThumbBuilder({ + @required double height, + @required Color backgroundColor, +}) { + final scrollThumb = Container( + decoration: BoxDecoration( + color: Colors.black26, + borderRadius: const BorderRadius.all( + Radius.circular(12.0), + ), + ), + height: height, + margin: const EdgeInsets.only(right: .5), + padding: const EdgeInsets.all(2), + child: ClipPath( + child: Container( + width: 20.0, + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: const BorderRadius.all( + Radius.circular(12.0), + ), + ), + ), + clipper: ArrowClipper(), + ), + ); return ( Color backgroundColor, Animation thumbAnimation, @@ -11,30 +39,6 @@ ScrollThumbBuilder avesScrollThumbBuilder() { double height, { Widget labelText, }) { - final scrollThumb = Container( - decoration: BoxDecoration( - color: Colors.black26, - borderRadius: const BorderRadius.all( - Radius.circular(12.0), - ), - ), - height: height, - margin: const EdgeInsets.only(right: .5), - padding: const EdgeInsets.all(2), - child: ClipPath( - child: Container( - width: 20.0, - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: const BorderRadius.all( - Radius.circular(12.0), - ), - ), - ), - clipper: ArrowClipper(), - ), - ); - return DraggableScrollbar.buildScrollThumbAndLabel( scrollThumb: scrollThumb, backgroundColor: backgroundColor, diff --git a/lib/widgets/fullscreen/fullscreen_action_delegate.dart b/lib/widgets/fullscreen/fullscreen_action_delegate.dart index 9fc5add43..6c4376ebe 100644 --- a/lib/widgets/fullscreen/fullscreen_action_delegate.dart +++ b/lib/widgets/fullscreen/fullscreen_action_delegate.dart @@ -82,7 +82,7 @@ class FullscreenActionDelegate { Future _print(ImageEntry entry) async { final uri = entry.uri; final mimeType = entry.mimeType; - final documentName = entry.title ?? 'Aves'; + final documentName = entry.bestTitle ?? 'Aves'; final doc = pdf.Document(title: documentName); PdfImage pdfImage; @@ -152,7 +152,7 @@ class FullscreenActionDelegate { } Future _showRenameDialog(BuildContext context, ImageEntry entry) async { - final currentName = entry.title; + final currentName = entry.bestTitle; final controller = TextEditingController(text: currentName); final newName = await showDialog( context: context, diff --git a/lib/widgets/fullscreen/info/basic_section.dart b/lib/widgets/fullscreen/info/basic_section.dart index f7fb8a762..3c46c0347 100644 --- a/lib/widgets/fullscreen/info/basic_section.dart +++ b/lib/widgets/fullscreen/info/basic_section.dart @@ -37,7 +37,7 @@ class BasicSection extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ InfoRowGroup({ - 'Title': entry.title ?? '?', + 'Title': entry.bestTitle ?? '?', 'Date': dateText, if (entry.isVideo) ..._buildVideoRows(), if (!entry.isSvg) 'Resolution': resolutionText, diff --git a/lib/widgets/fullscreen/overlay/bottom.dart b/lib/widgets/fullscreen/overlay/bottom.dart index 5920353c3..89de81bc5 100644 --- a/lib/widgets/fullscreen/overlay/bottom.dart +++ b/lib/widgets/fullscreen/overlay/bottom.dart @@ -152,7 +152,7 @@ class _FullscreenBottomOverlayContent extends AnimatedWidget { final subRowWidth = twoColumns ? min(_subRowMinWidth, maxWidth / 2) : maxWidth; final positionTitle = [ if (position != null) position, - if (entry.title != null) entry.title, + if (entry.bestTitle != null) entry.bestTitle, ].join(' – '); final hasShootingDetails = details != null && !details.isEmpty; return Column(