media store collection provider

This commit is contained in:
Thibault Deckers 2019-12-26 17:37:56 +09:00
parent dc14c354a8
commit 582afba3e9
7 changed files with 131 additions and 69 deletions

View file

@ -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/model/settings.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/album/all_collection_drawer.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/fake_app_bar.dart';
import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/common/media_query_data_provider.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/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_native_timezone/flutter_native_timezone.dart';
import 'package:pedantic/pedantic.dart'; import 'package:pedantic/pedantic.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:screen/screen.dart'; import 'package:screen/screen.dart';
final _stopwatch = Stopwatch()..start();
void main() { void main() {
debugPrint('main start, elapsed=${_stopwatch.elapsed}');
runApp(AvesApp()); runApp(AvesApp());
} }
@ -42,31 +35,32 @@ class AvesApp extends StatelessWidget {
), ),
), ),
), ),
home: HomePage(), home: const HomePage(),
); );
} }
} }
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
const HomePage();
@override @override
_HomePageState createState() => _HomePageState(); _HomePageState createState() => _HomePageState();
} }
class _HomePageState extends State<HomePage> { class _HomePageState extends State<HomePage> {
static const EventChannel eventChannel = EventChannel('deckers.thibault/aves/mediastore'); Future<void> _appSetup;
ImageCollection localMediaCollection = ImageCollection(entries: []);
@override @override
void initState() { void initState() {
debugPrint('$runtimeType initState');
super.initState(); super.initState();
_appSetup = _setup();
imageCache.maximumSizeBytes = 512 * (1 << 20); imageCache.maximumSizeBytes = 512 * (1 << 20);
setup();
Screen.keepOn(true); Screen.keepOn(true);
} }
Future<void> setup() async { Future<void> _setup() async {
debugPrint('$runtimeType setup start, elapsed=${_stopwatch.elapsed}'); debugPrint('$runtimeType _setup');
// TODO reduce permission check time // TODO reduce permission check time
// TODO TLAD ask android.permission.ACCESS_MEDIA_LOCATION (unredacted EXIF with scoped storage) // TODO TLAD ask android.permission.ACCESS_MEDIA_LOCATION (unredacted EXIF with scoped storage)
final permissions = await PermissionHandler().requestPermissions([ final permissions = await PermissionHandler().requestPermissions([
@ -76,57 +70,41 @@ class _HomePageState extends State<HomePage> {
unawaited(SystemNavigator.pop()); unawaited(SystemNavigator.pop());
return; return;
} }
// debugPrint('$runtimeType setup permission check done, elapsed=${stopwatch.elapsed}');
androidFileUtils.init(); androidFileUtils.init();
// debugPrint('$runtimeType setup androidFileUtils.init done, elapsed=${stopwatch.elapsed}');
// TODO notify when icons are ready for drawer and section header refresh // TODO notify when icons are ready for drawer and section header refresh
unawaited(IconUtils.init()); // 170ms unawaited(IconUtils.init()); // 170ms
// debugPrint('$runtimeType setup IconUtils.init done, elapsed=${stopwatch.elapsed}');
await settings.init(); // <20ms 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<Map>().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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MediaQueryDataProvider( return MediaQueryDataProvider(
child: FutureBuilder(
future: _appSetup,
builder: (futureContext, AsyncSnapshot<void> 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( child: Scaffold(
// fake app bar so that content is safe from status bar, even though we use a SliverAppBar // fake app bar so that content is safe from status bar, even though we use a SliverAppBar
appBar: FakeAppBar(), appBar: FakeAppBar(),
body: AllCollectionPage(collection: localMediaCollection), body: const AllCollectionPage(),
drawer: AllCollectionDrawer(collection: localMediaCollection), drawer: const AllCollectionDrawer(),
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
), ),
); );

View file

@ -64,6 +64,7 @@ class ImageCollection with ChangeNotifier {
]); ]);
break; break;
} }
debugPrint('$runtimeType updateSections');
notifyListeners(); notifyListeners();
} }

View file

@ -9,12 +9,11 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class AllCollectionDrawer extends StatelessWidget { class AllCollectionDrawer extends StatelessWidget {
final ImageCollection collection; const AllCollectionDrawer();
const AllCollectionDrawer({Key key, this.collection}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final collection = Provider.of<ImageCollection>(context);
final albums = collection.sortedAlbums; final albums = collection.sortedAlbums;
final tags = collection.sortedTags; final tags = collection.sortedTags;
return Drawer( return Drawer(

View file

@ -5,14 +5,15 @@ import 'package:aves/widgets/album/thumbnail_collection.dart';
import 'package:aves/widgets/common/menu_row.dart'; import 'package:aves/widgets/common/menu_row.dart';
import 'package:aves/widgets/debug_page.dart'; import 'package:aves/widgets/debug_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class AllCollectionPage extends StatelessWidget { class AllCollectionPage extends StatelessWidget {
final ImageCollection collection; const AllCollectionPage();
const AllCollectionPage({Key key, this.collection}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint('$runtimeType build');
final collection = Provider.of<ImageCollection>(context);
return ThumbnailCollection( return ThumbnailCollection(
collection: collection, collection: collection,
appBar: SliverAppBar( appBar: SliverAppBar(
@ -56,7 +57,7 @@ class AllCollectionPage extends StatelessWidget {
child: MenuRow(text: 'Debug', icon: Icons.whatshot), child: MenuRow(text: 'Debug', icon: Icons.whatshot),
), ),
], ],
onSelected: (action) => _onActionSelected(context, action), onSelected: (action) => _onActionSelected(context, collection, action),
), ),
], ],
floating: true, 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) { switch (action) {
case AlbumAction.debug: case AlbumAction.debug:
goToDebug(context); _goToDebug(context, collection);
break; break;
case AlbumAction.groupByAlbum: case AlbumAction.groupByAlbum:
settings.collectionGroupFactor = GroupFactor.album; 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( return Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(

View file

@ -23,11 +23,14 @@ class ThumbnailCollection extends AnimatedWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<MediaQueryData, double>( return Selector<MediaQueryData, double>(
selector: (c, mq) => mq.size.width, selector: (c, mq) => mq.size.width,
builder: (c, mqWidth, child) => ThumbnailCollectionContent( builder: (c, mqWidth, child) {
debugPrint('$runtimeType builder mqWidth=$mqWidth');
return ThumbnailCollectionContent(
collection: collection, collection: collection,
appBar: appBar, appBar: appBar,
screenWidth: mqWidth, screenWidth: mqWidth,
), );
},
); );
} }
} }
@ -126,7 +129,9 @@ class SectionSliver extends StatelessWidget {
), ),
sliver: SliverGrid( sliver: SliverGrid(
delegate: SliverChildBuilderDelegate( 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) { (sliverContext, index) {
final sectionEntries = sections[sectionKey]; final sectionEntries = sections[sectionKey];
if (index >= sectionEntries.length) return null; 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,
// ),
// ),
// );
} }
} }

View file

@ -35,6 +35,7 @@ class ImagePreviewState extends State<ImagePreview> with AfterInitMixin {
@override @override
void initState() { void initState() {
debugPrint('$runtimeType initState path=${entry.path}');
super.initState(); super.initState();
_entryChangeNotifier = Listenable.merge([ _entryChangeNotifier = Listenable.merge([
entry.imageChangeNotifier, entry.imageChangeNotifier,

View file

@ -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<ImageCollection> _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<Map>().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<ImageCollection>(
create: (context) => _create(),
initialData: ImageCollection(entries: []),
child: child,
);
}
}