From 582afba3e93ac7e533bc5116088ddb5faa993cb7 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 26 Dec 2019 17:37:56 +0900 Subject: [PATCH] media store collection provider --- lib/main.dart | 84 +++++++------------ lib/model/image_collection.dart | 1 + lib/widgets/album/all_collection_drawer.dart | 5 +- lib/widgets/album/all_collection_page.dart | 15 ++-- lib/widgets/album/thumbnail_collection.dart | 28 +++++-- lib/widgets/common/image_preview.dart | 1 + .../media_store_collection_provider.dart | 66 +++++++++++++++ 7 files changed, 131 insertions(+), 69 deletions(-) create mode 100644 lib/widgets/common/media_store_collection_provider.dart diff --git a/lib/main.dart b/lib/main.dart index c3d77e7e4..25c663ec3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,3 @@ -import 'package:aves/model/image_collection.dart'; -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/image_file_service.dart'; -import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/settings.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/album/all_collection_drawer.dart'; @@ -9,17 +5,14 @@ import 'package:aves/widgets/album/all_collection_page.dart'; import 'package:aves/widgets/common/fake_app_bar.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/media_query_data_provider.dart'; +import 'package:aves/widgets/common/media_store_collection_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_native_timezone/flutter_native_timezone.dart'; import 'package:pedantic/pedantic.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:screen/screen.dart'; -final _stopwatch = Stopwatch()..start(); - void main() { - debugPrint('main start, elapsed=${_stopwatch.elapsed}'); runApp(AvesApp()); } @@ -42,31 +35,32 @@ class AvesApp extends StatelessWidget { ), ), ), - home: HomePage(), + home: const HomePage(), ); } } class HomePage extends StatefulWidget { + const HomePage(); + @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State { - static const EventChannel eventChannel = EventChannel('deckers.thibault/aves/mediastore'); - - ImageCollection localMediaCollection = ImageCollection(entries: []); + Future _appSetup; @override void initState() { + debugPrint('$runtimeType initState'); super.initState(); + _appSetup = _setup(); imageCache.maximumSizeBytes = 512 * (1 << 20); - setup(); Screen.keepOn(true); } - Future setup() async { - debugPrint('$runtimeType setup start, elapsed=${_stopwatch.elapsed}'); + Future _setup() async { + debugPrint('$runtimeType _setup'); // TODO reduce permission check time // TODO TLAD ask android.permission.ACCESS_MEDIA_LOCATION (unredacted EXIF with scoped storage) final permissions = await PermissionHandler().requestPermissions([ @@ -76,57 +70,41 @@ class _HomePageState extends State { unawaited(SystemNavigator.pop()); return; } -// debugPrint('$runtimeType setup permission check done, elapsed=${stopwatch.elapsed}'); androidFileUtils.init(); -// debugPrint('$runtimeType setup androidFileUtils.init done, elapsed=${stopwatch.elapsed}'); // TODO notify when icons are ready for drawer and section header refresh unawaited(IconUtils.init()); // 170ms -// debugPrint('$runtimeType setup IconUtils.init done, elapsed=${stopwatch.elapsed}'); + await settings.init(); // <20ms - localMediaCollection.groupFactor = settings.collectionGroupFactor; - localMediaCollection.sortFactor = settings.collectionSortFactor; - debugPrint('$runtimeType setup settings.init done, elapsed=${_stopwatch.elapsed}'); - - await metadataDb.init(); // <20ms - final currentTimeZone = await FlutterNativeTimezone.getLocalTimezone(); // <20ms - final catalogTimeZone = settings.catalogTimeZone; - if (currentTimeZone != catalogTimeZone) { - // clear catalog metadata to get correct date/times when moving to a different time zone - debugPrint('$runtimeType clear catalog metadata to get correct date/times'); - await metadataDb.clearMetadataEntries(); - settings.catalogTimeZone = currentTimeZone; - } -// debugPrint('$runtimeType setup metadataDb.init done, elapsed=${stopwatch.elapsed}'); - - eventChannel.receiveBroadcastStream().cast().listen( - (entryMap) => localMediaCollection.add(ImageEntry.fromMap(entryMap)), - onDone: () async { - debugPrint('$runtimeType mediastore stream done, elapsed=${_stopwatch.elapsed}'); - localMediaCollection.updateSections(); // <50ms - // TODO reduce setup time until here - localMediaCollection.updateAlbums(); // <50ms - await localMediaCollection.loadCatalogMetadata(); // 650ms - await localMediaCollection.catalogEntries(); // <50ms - await localMediaCollection.loadAddresses(); // 350ms - await localMediaCollection.locateEntries(); // <50ms - debugPrint('$runtimeType setup end, elapsed=${_stopwatch.elapsed}'); - }, - onError: (error) => debugPrint('$runtimeType mediastore stream error=$error'), - ); -// debugPrint('$runtimeType setup fetch images, elapsed=${stopwatch.elapsed}'); - // TODO split image fetch AND/OR cache fetch across sessions - await ImageFileService.getImageEntries(); // 460ms } @override Widget build(BuildContext context) { return MediaQueryDataProvider( + child: FutureBuilder( + future: _appSetup, + builder: (futureContext, AsyncSnapshot snapshot) { + if (snapshot.hasError) return const Icon(Icons.error); + if (snapshot.connectionState != ConnectionState.done) return const CircularProgressIndicator(); + debugPrint('$runtimeType FutureBuilder builder'); + return const MediaStoreCollectionPage(); + }), + ); + } +} + +class MediaStoreCollectionPage extends StatelessWidget { + const MediaStoreCollectionPage(); + + @override + Widget build(BuildContext context) { + debugPrint('$runtimeType build'); + return MediaStoreCollectionProvider( child: Scaffold( // fake app bar so that content is safe from status bar, even though we use a SliverAppBar appBar: FakeAppBar(), - body: AllCollectionPage(collection: localMediaCollection), - drawer: AllCollectionDrawer(collection: localMediaCollection), + body: const AllCollectionPage(), + drawer: const AllCollectionDrawer(), resizeToAvoidBottomInset: false, ), ); diff --git a/lib/model/image_collection.dart b/lib/model/image_collection.dart index 7184782e9..da2194f8c 100644 --- a/lib/model/image_collection.dart +++ b/lib/model/image_collection.dart @@ -64,6 +64,7 @@ class ImageCollection with ChangeNotifier { ]); break; } + debugPrint('$runtimeType updateSections'); notifyListeners(); } diff --git a/lib/widgets/album/all_collection_drawer.dart b/lib/widgets/album/all_collection_drawer.dart index a44f40f8c..bdf34194e 100644 --- a/lib/widgets/album/all_collection_drawer.dart +++ b/lib/widgets/album/all_collection_drawer.dart @@ -9,12 +9,11 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; class AllCollectionDrawer extends StatelessWidget { - final ImageCollection collection; - - const AllCollectionDrawer({Key key, this.collection}) : super(key: key); + const AllCollectionDrawer(); @override Widget build(BuildContext context) { + final collection = Provider.of(context); final albums = collection.sortedAlbums; final tags = collection.sortedTags; return Drawer( diff --git a/lib/widgets/album/all_collection_page.dart b/lib/widgets/album/all_collection_page.dart index 3c034a343..d6a074169 100644 --- a/lib/widgets/album/all_collection_page.dart +++ b/lib/widgets/album/all_collection_page.dart @@ -5,14 +5,15 @@ import 'package:aves/widgets/album/thumbnail_collection.dart'; import 'package:aves/widgets/common/menu_row.dart'; import 'package:aves/widgets/debug_page.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class AllCollectionPage extends StatelessWidget { - final ImageCollection collection; - - const AllCollectionPage({Key key, this.collection}) : super(key: key); + const AllCollectionPage(); @override Widget build(BuildContext context) { + debugPrint('$runtimeType build'); + final collection = Provider.of(context); return ThumbnailCollection( collection: collection, appBar: SliverAppBar( @@ -56,7 +57,7 @@ class AllCollectionPage extends StatelessWidget { child: MenuRow(text: 'Debug', icon: Icons.whatshot), ), ], - onSelected: (action) => _onActionSelected(context, action), + onSelected: (action) => _onActionSelected(context, collection, action), ), ], floating: true, @@ -64,10 +65,10 @@ class AllCollectionPage extends StatelessWidget { ); } - void _onActionSelected(BuildContext context, AlbumAction action) { + void _onActionSelected(BuildContext context, ImageCollection collection, AlbumAction action) { switch (action) { case AlbumAction.debug: - goToDebug(context); + _goToDebug(context, collection); break; case AlbumAction.groupByAlbum: settings.collectionGroupFactor = GroupFactor.album; @@ -92,7 +93,7 @@ class AllCollectionPage extends StatelessWidget { } } - Future goToDebug(BuildContext context) { + Future _goToDebug(BuildContext context, ImageCollection collection) { return Navigator.push( context, MaterialPageRoute( diff --git a/lib/widgets/album/thumbnail_collection.dart b/lib/widgets/album/thumbnail_collection.dart index af4f25fed..51194e795 100644 --- a/lib/widgets/album/thumbnail_collection.dart +++ b/lib/widgets/album/thumbnail_collection.dart @@ -23,11 +23,14 @@ class ThumbnailCollection extends AnimatedWidget { Widget build(BuildContext context) { return Selector( selector: (c, mq) => mq.size.width, - builder: (c, mqWidth, child) => ThumbnailCollectionContent( - collection: collection, - appBar: appBar, - screenWidth: mqWidth, - ), + builder: (c, mqWidth, child) { + debugPrint('$runtimeType builder mqWidth=$mqWidth'); + return ThumbnailCollectionContent( + collection: collection, + appBar: appBar, + screenWidth: mqWidth, + ); + }, ); } } @@ -126,7 +129,9 @@ class SectionSliver extends StatelessWidget { ), sliver: SliverGrid( delegate: SliverChildBuilderDelegate( - // TODO TLAD find out why thumbnails are rebuilt when config change (show/hide status bar) + // TODO TLAD find out why thumbnails are rebuilt (with `initState`) when: + // - config change (show/hide status bar) + // - navigating away/back (sliverContext, index) { final sectionEntries = sections[sectionKey]; if (index >= sectionEntries.length) return null; @@ -161,6 +166,17 @@ class SectionSliver extends StatelessWidget { ), ), ); + // TODO TLAD consider the following to have transparency while popping fullscreen by drag down +// Navigator.push( +// context, +// PageRouteBuilder( +// opaque: false, +// pageBuilder: (BuildContext context, _, __) => FullscreenPage( +// collection: collection, +// initialUri: entry.uri, +// ), +// ), +// ); } } diff --git a/lib/widgets/common/image_preview.dart b/lib/widgets/common/image_preview.dart index 48f9f9020..74e058f1b 100644 --- a/lib/widgets/common/image_preview.dart +++ b/lib/widgets/common/image_preview.dart @@ -35,6 +35,7 @@ class ImagePreviewState extends State with AfterInitMixin { @override void initState() { + debugPrint('$runtimeType initState path=${entry.path}'); super.initState(); _entryChangeNotifier = Listenable.merge([ entry.imageChangeNotifier, diff --git a/lib/widgets/common/media_store_collection_provider.dart b/lib/widgets/common/media_store_collection_provider.dart new file mode 100644 index 000000000..56a9396d9 --- /dev/null +++ b/lib/widgets/common/media_store_collection_provider.dart @@ -0,0 +1,66 @@ +import 'package:aves/model/image_collection.dart'; +import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/image_file_service.dart'; +import 'package:aves/model/metadata_db.dart'; +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'; + +final _stopwatch = Stopwatch()..start(); + +class MediaStoreCollectionProvider extends StatelessWidget { + final Widget child; + + static const EventChannel eventChannel = EventChannel('deckers.thibault/aves/mediastore'); + + const MediaStoreCollectionProvider({@required this.child}); + + Future _create() async { + debugPrint('$runtimeType _create, elapsed=${_stopwatch.elapsed}'); + final mediaStoreCollection = ImageCollection(entries: []); + mediaStoreCollection.groupFactor = settings.collectionGroupFactor; + mediaStoreCollection.sortFactor = settings.collectionSortFactor; + + await metadataDb.init(); // <20ms + final currentTimeZone = await FlutterNativeTimezone.getLocalTimezone(); // <20ms + final catalogTimeZone = settings.catalogTimeZone; + if (currentTimeZone != catalogTimeZone) { + // clear catalog metadata to get correct date/times when moving to a different time zone + debugPrint('$runtimeType clear catalog metadata to get correct date/times'); + await metadataDb.clearMetadataEntries(); + settings.catalogTimeZone = currentTimeZone; + } + + eventChannel.receiveBroadcastStream().cast().listen( + (entryMap) => mediaStoreCollection.add(ImageEntry.fromMap(entryMap)), + onDone: () async { + debugPrint('$runtimeType mediastore stream done, elapsed=${_stopwatch.elapsed}'); + mediaStoreCollection.updateSections(); // <50ms + // TODO reduce setup time until here + mediaStoreCollection.updateAlbums(); // <50ms + await mediaStoreCollection.loadCatalogMetadata(); // 650ms + await mediaStoreCollection.catalogEntries(); // <50ms + await mediaStoreCollection.loadAddresses(); // 350ms + await mediaStoreCollection.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 + await ImageFileService.getImageEntries(); // 460ms + + return mediaStoreCollection; + } + + @override + Widget build(BuildContext context) { + return FutureProvider( + create: (context) => _create(), + initialData: ImageCollection(entries: []), + child: child, + ); + } +}