From be66415842b34f1902a31cfd86b4b1ee405d29a3 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 16 Aug 2019 18:49:07 +0900 Subject: [PATCH] model: added collection --- lib/main.dart | 75 +++---------------- lib/model/image_collection.dart | 79 +++++++++++++++++++++ lib/model/image_entry.dart | 2 - lib/widgets/album/all_collection_page.dart | 12 ++-- lib/widgets/album/search_delegate.dart | 9 +-- lib/widgets/album/thumbnail_collection.dart | 39 +++++++--- lib/widgets/fullscreen/image_page.dart | 36 +++++----- 7 files changed, 148 insertions(+), 104 deletions(-) create mode 100644 lib/model/image_collection.dart diff --git a/lib/main.dart b/lib/main.dart index e18776541..b1f4b6ffa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,6 @@ +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/image_metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/widgets/album/all_collection_page.dart'; import 'package:aves/widgets/common/fake_app_bar.dart'; @@ -34,7 +34,7 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { static const EventChannel eventChannel = EventChannel('deckers.thibault/aves/mediastore'); - List entries = List(); + ImageCollection localMediaCollection = ImageCollection(List()); @override void initState() { @@ -47,15 +47,15 @@ class _HomePageState extends State { await metadataDb.init(); eventChannel.receiveBroadcastStream().cast().listen( - (entryMap) => entries.add(ImageEntry.fromMap(entryMap)), + (entryMap) => localMediaCollection.entries.add(ImageEntry.fromMap(entryMap)), onDone: () async { debugPrint('mediastore stream done'); - await loadCatalogMetadata(); + await localMediaCollection.loadCatalogMetadata(); setState(() {}); - await catalogEntries(); + await localMediaCollection.catalogEntries(); setState(() {}); - await loadAddresses(); - await locateEntries(); + await localMediaCollection.loadAddresses(); + await localMediaCollection.locateEntries(); }, onError: (error) => debugPrint('mediastore stream error=$error'), ); @@ -67,67 +67,8 @@ class _HomePageState extends State { return Scaffold( // fake app bar so that content is safe from status bar, even though we use a SliverAppBar appBar: FakeAppBar(), - body: AllCollectionPage(entries: entries), + body: AllCollectionPage(collection: localMediaCollection), resizeToAvoidBottomInset: false, ); } - - loadCatalogMetadata() async { - debugPrint('$runtimeType loadCatalogMetadata start'); - final start = DateTime.now(); - final saved = await metadataDb.loadMetadataEntries(); - entries.forEach((entry) { - final contentId = entry.contentId; - if (contentId != null) { - entry.catalogMetadata = saved.firstWhere((metadata) => metadata.contentId == contentId, orElse: () => null); - } - }); - debugPrint('$runtimeType loadCatalogMetadata complete in ${DateTime.now().difference(start).inSeconds}s with ${saved.length} saved entries'); - } - - loadAddresses() async { - debugPrint('$runtimeType loadAddresses start'); - final start = DateTime.now(); - final saved = await metadataDb.loadAddresses(); - entries.forEach((entry) { - final contentId = entry.contentId; - if (contentId != null) { - entry.addressDetails = saved.firstWhere((address) => address.contentId == contentId, orElse: () => null); - } - }); - debugPrint('$runtimeType loadAddresses complete in ${DateTime.now().difference(start).inSeconds}s with ${saved.length} saved entries'); - } - - catalogEntries() async { - debugPrint('$runtimeType catalogEntries start'); - final start = DateTime.now(); - final uncataloguedEntries = entries.where((entry) => !entry.isCatalogued); - final newMetadata = List(); - await Future.forEach(uncataloguedEntries, (entry) async { - await entry.catalog(); - newMetadata.add(entry.catalogMetadata); - }); - debugPrint('$runtimeType catalogEntries complete in ${DateTime.now().difference(start).inSeconds}s with ${newMetadata.length} new entries'); - - // sort with more accurate date - entries.sort((a, b) => b.bestDate.compareTo(a.bestDate)); - - metadataDb.saveMetadata(List.unmodifiable(newMetadata)); - } - - locateEntries() async { - debugPrint('$runtimeType locateEntries start'); - final start = DateTime.now(); - final unlocatedEntries = entries.where((entry) => !entry.isLocated); - final newAddresses = List(); - await Future.forEach(unlocatedEntries, (entry) async { - await entry.locate(); - newAddresses.add(entry.addressDetails); - if (newAddresses.length >= 50) { - metadataDb.saveAddresses(List.unmodifiable(newAddresses)); - newAddresses.clear(); - } - }); - debugPrint('$runtimeType locateEntries complete in ${DateTime.now().difference(start).inSeconds}s'); - } } diff --git a/lib/model/image_collection.dart b/lib/model/image_collection.dart new file mode 100644 index 000000000..89e3d37e0 --- /dev/null +++ b/lib/model/image_collection.dart @@ -0,0 +1,79 @@ +import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/image_file_service.dart'; +import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/metadata_db.dart'; +import 'package:flutter/material.dart'; + +class ImageCollection with ChangeNotifier { + final List entries; + + ImageCollection(this.entries); + + Future delete(ImageEntry entry) async { + final success = await ImageFileService.delete(entry); + if (success) { + entries.remove(entry); + notifyListeners(); + } + return success; + } + + loadCatalogMetadata() async { + debugPrint('$runtimeType loadCatalogMetadata start'); + final start = DateTime.now(); + final saved = await metadataDb.loadMetadataEntries(); + entries.forEach((entry) { + final contentId = entry.contentId; + if (contentId != null) { + entry.catalogMetadata = saved.firstWhere((metadata) => metadata.contentId == contentId, orElse: () => null); + } + }); + debugPrint('$runtimeType loadCatalogMetadata complete in ${DateTime.now().difference(start).inSeconds}s with ${saved.length} saved entries'); + } + + loadAddresses() async { + debugPrint('$runtimeType loadAddresses start'); + final start = DateTime.now(); + final saved = await metadataDb.loadAddresses(); + entries.forEach((entry) { + final contentId = entry.contentId; + if (contentId != null) { + entry.addressDetails = saved.firstWhere((address) => address.contentId == contentId, orElse: () => null); + } + }); + debugPrint('$runtimeType loadAddresses complete in ${DateTime.now().difference(start).inSeconds}s with ${saved.length} saved entries'); + } + + catalogEntries() async { + debugPrint('$runtimeType catalogEntries start'); + final start = DateTime.now(); + final uncataloguedEntries = entries.where((entry) => !entry.isCatalogued); + final newMetadata = List(); + await Future.forEach(uncataloguedEntries, (entry) async { + await entry.catalog(); + newMetadata.add(entry.catalogMetadata); + }); + debugPrint('$runtimeType catalogEntries complete in ${DateTime.now().difference(start).inSeconds}s with ${newMetadata.length} new entries'); + + // sort with more accurate date + entries.sort((a, b) => b.bestDate.compareTo(a.bestDate)); + + metadataDb.saveMetadata(List.unmodifiable(newMetadata)); + } + + locateEntries() async { + debugPrint('$runtimeType locateEntries start'); + final start = DateTime.now(); + final unlocatedEntries = entries.where((entry) => !entry.isLocated); + final newAddresses = List(); + await Future.forEach(unlocatedEntries, (entry) async { + await entry.locate(); + newAddresses.add(entry.addressDetails); + if (newAddresses.length >= 50) { + metadataDb.saveAddresses(List.unmodifiable(newAddresses)); + newAddresses.clear(); + } + }); + debugPrint('$runtimeType locateEntries complete in ${DateTime.now().difference(start).inSeconds}s'); + } +} diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index 4de8fed7d..eca6e4a73 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -188,8 +188,6 @@ class ImageEntry with ChangeNotifier { return false; } - Future delete() => ImageFileService.delete(this); - Future rename(String newName) async { if (newName == filename) return true; diff --git a/lib/widgets/album/all_collection_page.dart b/lib/widgets/album/all_collection_page.dart index 50d3cc79f..cb1b556e2 100644 --- a/lib/widgets/album/all_collection_page.dart +++ b/lib/widgets/album/all_collection_page.dart @@ -1,18 +1,18 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/image_collection.dart'; import 'package:aves/widgets/album/search_delegate.dart'; import 'package:aves/widgets/album/thumbnail_collection.dart'; import 'package:aves/widgets/debug_page.dart'; import 'package:flutter/material.dart'; class AllCollectionPage extends StatelessWidget { - final List entries; + final ImageCollection collection; - const AllCollectionPage({Key key, this.entries}) : super(key: key); + const AllCollectionPage({Key key, this.collection}) : super(key: key); @override Widget build(BuildContext context) { return ThumbnailCollection( - entries: entries, + collection: collection, appBar: SliverAppBar( title: Text('Aves - All'), actions: [ @@ -20,7 +20,7 @@ class AllCollectionPage extends StatelessWidget { icon: Icon(Icons.search), onPressed: () => showSearch( context: context, - delegate: ImageSearchDelegate(entries), + delegate: ImageSearchDelegate(collection), ), ), IconButton(icon: Icon(Icons.whatshot), onPressed: () => goToDebug(context)), @@ -35,7 +35,7 @@ class AllCollectionPage extends StatelessWidget { context, MaterialPageRoute( builder: (context) => DebugPage( - entries: entries, + entries: collection.entries, ), ), ); diff --git a/lib/widgets/album/search_delegate.dart b/lib/widgets/album/search_delegate.dart index 813896d05..43c8df23d 100644 --- a/lib/widgets/album/search_delegate.dart +++ b/lib/widgets/album/search_delegate.dart @@ -1,11 +1,12 @@ +import 'package:aves/model/image_collection.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/widgets/album/thumbnail_collection.dart'; import 'package:flutter/material.dart'; class ImageSearchDelegate extends SearchDelegate { - final List entries; + final ImageCollection collection; - ImageSearchDelegate(this.entries); + ImageSearchDelegate(this.collection); @override ThemeData appBarTheme(BuildContext context) { @@ -51,7 +52,7 @@ class ImageSearchDelegate extends SearchDelegate { return SizedBox.shrink(); } final lowerQuery = query.toLowerCase(); - final matches = entries.where((entry) => entry.search(lowerQuery)).toList(); + final matches = collection.entries.where((entry) => entry.search(lowerQuery)).toList(); if (matches.isEmpty) { return Center( child: Text( @@ -60,6 +61,6 @@ class ImageSearchDelegate extends SearchDelegate { ), ); } - return ThumbnailCollection(entries: matches); + return ThumbnailCollection(collection: ImageCollection(matches)); } } diff --git a/lib/widgets/album/thumbnail_collection.dart b/lib/widgets/album/thumbnail_collection.dart index a2b4b1dd9..7f1c2376d 100644 --- a/lib/widgets/album/thumbnail_collection.dart +++ b/lib/widgets/album/thumbnail_collection.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/image_collection.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/utils/date_utils.dart'; import 'package:aves/widgets/album/thumbnail.dart'; @@ -9,15 +10,37 @@ import 'package:flutter/material.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:intl/intl.dart'; -class ThumbnailCollection extends StatelessWidget { - final List entries; +class ThumbnailCollection extends AnimatedWidget { + final ImageCollection collection; + final Widget appBar; + + ThumbnailCollection({ + Key key, + this.collection, + this.appBar, + }) : super(key: key, listenable: collection); + + @override + Widget build(BuildContext context) { + return ThumbnailCollectionContent( + collection: collection, + appBar: appBar, + ); + } +} + +class ThumbnailCollectionContent extends StatelessWidget { + final ImageCollection collection; final Widget appBar; final Map> _sections; final ScrollController _scrollController = ScrollController(); - ThumbnailCollection({Key key, this.entries, this.appBar}) - : _sections = groupBy(entries, (entry) => entry.monthTaken), + ThumbnailCollectionContent({ + Key key, + this.collection, + this.appBar, + }) : _sections = groupBy(collection.entries, (entry) => entry.monthTaken), super(key: key); @override @@ -32,7 +55,7 @@ class ThumbnailCollection extends StatelessWidget { if (appBar != null) appBar, ...sectionKeys.map((sectionKey) { Widget sliver = SectionSliver( - entries: entries, + collection: collection, sections: _sections, sectionKey: sectionKey, ); @@ -58,13 +81,13 @@ class ThumbnailCollection extends StatelessWidget { } class SectionSliver extends StatelessWidget { - final List entries; + final ImageCollection collection; final Map> sections; final DateTime sectionKey; const SectionSliver({ Key key, - this.entries, + this.collection, this.sections, this.sectionKey, }) : super(key: key); @@ -105,7 +128,7 @@ class SectionSliver extends StatelessWidget { context, MaterialPageRoute( builder: (context) => FullscreenPage( - entries: entries, + collection: collection, initialUri: entry.uri, ), ), diff --git a/lib/widgets/fullscreen/image_page.dart b/lib/widgets/fullscreen/image_page.dart index 7e85a78fd..c0269e687 100644 --- a/lib/widgets/fullscreen/image_page.dart +++ b/lib/widgets/fullscreen/image_page.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:math'; +import 'package:aves/model/image_collection.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/utils/android_app_service.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart'; @@ -15,15 +16,15 @@ import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view_gallery.dart'; import 'package:screen/screen.dart'; -class FullscreenPage extends StatelessWidget { - final List entries; +class FullscreenPage extends AnimatedWidget { + final ImageCollection collection; final String initialUri; const FullscreenPage({ Key key, - this.entries, + this.collection, this.initialUri, - }) : super(key: key); + }) : super(key: key, listenable: collection); @override Widget build(BuildContext context) { @@ -36,7 +37,7 @@ class FullscreenPage extends StatelessWidget { child: Scaffold( backgroundColor: Colors.black, body: FullscreenBody( - entries: entries, + collection: collection, initialUri: initialUri, ), resizeToAvoidBottomInset: false, @@ -74,12 +75,12 @@ class FullscreenPage extends StatelessWidget { } class FullscreenBody extends StatefulWidget { - final List entries; + final ImageCollection collection; final String initialUri; const FullscreenBody({ Key key, - this.entries, + this.collection, this.initialUri, }) : super(key: key); @@ -97,7 +98,9 @@ class FullscreenBodyState extends State with SingleTickerProvide Animation _bottomOverlayOffset; EdgeInsets _frozenViewInsets, _frozenViewPadding; - List get entries => widget.entries; + ImageCollection get collection => widget.collection; + + List get entries => widget.collection.entries; @override void initState() { @@ -149,7 +152,7 @@ class FullscreenBodyState extends State with SingleTickerProvide onPageChanged: (page) => setState(() => _currentVerticalPage = page), children: [ ImagePage( - entries: entries, + collection: collection, pageController: _horizontalPager, onTap: () => _overlayVisible.value = !_overlayVisible.value, onPageChanged: (page) => setState(() => _currentHorizontalPage = page), @@ -286,10 +289,7 @@ class FullscreenBodyState extends State with SingleTickerProvide }, ); if (confirmed == null || !confirmed) return; - if (await entry.delete()) - entries.remove(entry); - else - showFeedback('Failed'); + if (!await collection.delete(entry)) showFeedback('Failed'); } showRenameDialog(ImageEntry entry) async { @@ -323,14 +323,14 @@ class FullscreenBodyState extends State with SingleTickerProvide enum FullscreenAction { delete, edit, info, open, openMap, rename, rotateCCW, rotateCW, setAs, share } class ImagePage extends StatefulWidget { - final List entries; + final ImageCollection collection; final PageController pageController; final VoidCallback onTap; final ValueChanged onPageChanged; final ValueChanged onScaleChanged; const ImagePage({ - this.entries, + this.collection, this.pageController, this.onTap, this.onPageChanged, @@ -342,13 +342,15 @@ class ImagePage extends StatefulWidget { } class ImagePageState extends State with AutomaticKeepAliveClientMixin { + List get entries => widget.collection.entries; + @override Widget build(BuildContext context) { super.build(context); return PhotoViewGallery.builder( - itemCount: widget.entries.length, + itemCount: entries.length, builder: (galleryContext, index) { - final entry = widget.entries[index]; + final entry = entries[index]; if (entry.isVideo) { return PhotoViewGalleryPageOptions.customChild( child: AvesVideo(entry: entry),