media store collection provider
This commit is contained in:
parent
dc14c354a8
commit
582afba3e9
7 changed files with 131 additions and 69 deletions
|
@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -64,6 +64,7 @@ class ImageCollection with ChangeNotifier {
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
debugPrint('$runtimeType updateSections');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
66
lib/widgets/common/media_store_collection_provider.dart
Normal file
66
lib/widgets/common/media_store_collection_provider.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue