model: added collection

This commit is contained in:
Thibault Deckers 2019-08-16 18:49:07 +09:00
parent 7d2a27f797
commit be66415842
7 changed files with 148 additions and 104 deletions

View file

@ -1,6 +1,6 @@
import 'package:aves/model/image_collection.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/model/image_file_service.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/model/metadata_db.dart';
import 'package:aves/widgets/album/all_collection_page.dart'; 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';
@ -34,7 +34,7 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> { class _HomePageState extends State<HomePage> {
static const EventChannel eventChannel = EventChannel('deckers.thibault/aves/mediastore'); static const EventChannel eventChannel = EventChannel('deckers.thibault/aves/mediastore');
List<ImageEntry> entries = List(); ImageCollection localMediaCollection = ImageCollection(List());
@override @override
void initState() { void initState() {
@ -47,15 +47,15 @@ class _HomePageState extends State<HomePage> {
await metadataDb.init(); await metadataDb.init();
eventChannel.receiveBroadcastStream().cast<Map>().listen( eventChannel.receiveBroadcastStream().cast<Map>().listen(
(entryMap) => entries.add(ImageEntry.fromMap(entryMap)), (entryMap) => localMediaCollection.entries.add(ImageEntry.fromMap(entryMap)),
onDone: () async { onDone: () async {
debugPrint('mediastore stream done'); debugPrint('mediastore stream done');
await loadCatalogMetadata(); await localMediaCollection.loadCatalogMetadata();
setState(() {}); setState(() {});
await catalogEntries(); await localMediaCollection.catalogEntries();
setState(() {}); setState(() {});
await loadAddresses(); await localMediaCollection.loadAddresses();
await locateEntries(); await localMediaCollection.locateEntries();
}, },
onError: (error) => debugPrint('mediastore stream error=$error'), onError: (error) => debugPrint('mediastore stream error=$error'),
); );
@ -67,67 +67,8 @@ class _HomePageState extends State<HomePage> {
return Scaffold( return 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(entries: entries), body: AllCollectionPage(collection: localMediaCollection),
resizeToAvoidBottomInset: false, 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<CatalogMetadata>();
await Future.forEach<ImageEntry>(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<AddressDetails>();
await Future.forEach<ImageEntry>(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');
}
} }

View file

@ -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<ImageEntry> entries;
ImageCollection(this.entries);
Future<bool> 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<CatalogMetadata>();
await Future.forEach<ImageEntry>(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<AddressDetails>();
await Future.forEach<ImageEntry>(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');
}
}

View file

@ -188,8 +188,6 @@ class ImageEntry with ChangeNotifier {
return false; return false;
} }
Future<bool> delete() => ImageFileService.delete(this);
Future<bool> rename(String newName) async { Future<bool> rename(String newName) async {
if (newName == filename) return true; if (newName == filename) return true;

View file

@ -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/search_delegate.dart';
import 'package:aves/widgets/album/thumbnail_collection.dart'; import 'package:aves/widgets/album/thumbnail_collection.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';
class AllCollectionPage extends StatelessWidget { class AllCollectionPage extends StatelessWidget {
final List<ImageEntry> entries; final ImageCollection collection;
const AllCollectionPage({Key key, this.entries}) : super(key: key); const AllCollectionPage({Key key, this.collection}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ThumbnailCollection( return ThumbnailCollection(
entries: entries, collection: collection,
appBar: SliverAppBar( appBar: SliverAppBar(
title: Text('Aves - All'), title: Text('Aves - All'),
actions: [ actions: [
@ -20,7 +20,7 @@ class AllCollectionPage extends StatelessWidget {
icon: Icon(Icons.search), icon: Icon(Icons.search),
onPressed: () => showSearch( onPressed: () => showSearch(
context: context, context: context,
delegate: ImageSearchDelegate(entries), delegate: ImageSearchDelegate(collection),
), ),
), ),
IconButton(icon: Icon(Icons.whatshot), onPressed: () => goToDebug(context)), IconButton(icon: Icon(Icons.whatshot), onPressed: () => goToDebug(context)),
@ -35,7 +35,7 @@ class AllCollectionPage extends StatelessWidget {
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => DebugPage( builder: (context) => DebugPage(
entries: entries, entries: collection.entries,
), ),
), ),
); );

View file

@ -1,11 +1,12 @@
import 'package:aves/model/image_collection.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/album/thumbnail_collection.dart'; import 'package:aves/widgets/album/thumbnail_collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ImageSearchDelegate extends SearchDelegate<ImageEntry> { class ImageSearchDelegate extends SearchDelegate<ImageEntry> {
final List<ImageEntry> entries; final ImageCollection collection;
ImageSearchDelegate(this.entries); ImageSearchDelegate(this.collection);
@override @override
ThemeData appBarTheme(BuildContext context) { ThemeData appBarTheme(BuildContext context) {
@ -51,7 +52,7 @@ class ImageSearchDelegate extends SearchDelegate<ImageEntry> {
return SizedBox.shrink(); return SizedBox.shrink();
} }
final lowerQuery = query.toLowerCase(); 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) { if (matches.isEmpty) {
return Center( return Center(
child: Text( child: Text(
@ -60,6 +61,6 @@ class ImageSearchDelegate extends SearchDelegate<ImageEntry> {
), ),
); );
} }
return ThumbnailCollection(entries: matches); return ThumbnailCollection(collection: ImageCollection(matches));
} }
} }

View file

@ -1,3 +1,4 @@
import 'package:aves/model/image_collection.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/utils/date_utils.dart'; import 'package:aves/utils/date_utils.dart';
import 'package:aves/widgets/album/thumbnail.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:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class ThumbnailCollection extends StatelessWidget { class ThumbnailCollection extends AnimatedWidget {
final List<ImageEntry> entries; 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 Widget appBar;
final Map<DateTime, List<ImageEntry>> _sections; final Map<DateTime, List<ImageEntry>> _sections;
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
ThumbnailCollection({Key key, this.entries, this.appBar}) ThumbnailCollectionContent({
: _sections = groupBy(entries, (entry) => entry.monthTaken), Key key,
this.collection,
this.appBar,
}) : _sections = groupBy(collection.entries, (entry) => entry.monthTaken),
super(key: key); super(key: key);
@override @override
@ -32,7 +55,7 @@ class ThumbnailCollection extends StatelessWidget {
if (appBar != null) appBar, if (appBar != null) appBar,
...sectionKeys.map((sectionKey) { ...sectionKeys.map((sectionKey) {
Widget sliver = SectionSliver( Widget sliver = SectionSliver(
entries: entries, collection: collection,
sections: _sections, sections: _sections,
sectionKey: sectionKey, sectionKey: sectionKey,
); );
@ -58,13 +81,13 @@ class ThumbnailCollection extends StatelessWidget {
} }
class SectionSliver extends StatelessWidget { class SectionSliver extends StatelessWidget {
final List<ImageEntry> entries; final ImageCollection collection;
final Map<DateTime, List<ImageEntry>> sections; final Map<DateTime, List<ImageEntry>> sections;
final DateTime sectionKey; final DateTime sectionKey;
const SectionSliver({ const SectionSliver({
Key key, Key key,
this.entries, this.collection,
this.sections, this.sections,
this.sectionKey, this.sectionKey,
}) : super(key: key); }) : super(key: key);
@ -105,7 +128,7 @@ class SectionSliver extends StatelessWidget {
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => FullscreenPage( builder: (context) => FullscreenPage(
entries: entries, collection: collection,
initialUri: entry.uri, initialUri: entry.uri,
), ),
), ),

View file

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:aves/model/image_collection.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/utils/android_app_service.dart'; import 'package:aves/utils/android_app_service.dart';
import 'package:aves/widgets/fullscreen/info/info_page.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:photo_view/photo_view_gallery.dart';
import 'package:screen/screen.dart'; import 'package:screen/screen.dart';
class FullscreenPage extends StatelessWidget { class FullscreenPage extends AnimatedWidget {
final List<ImageEntry> entries; final ImageCollection collection;
final String initialUri; final String initialUri;
const FullscreenPage({ const FullscreenPage({
Key key, Key key,
this.entries, this.collection,
this.initialUri, this.initialUri,
}) : super(key: key); }) : super(key: key, listenable: collection);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -36,7 +37,7 @@ class FullscreenPage extends StatelessWidget {
child: Scaffold( child: Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.black,
body: FullscreenBody( body: FullscreenBody(
entries: entries, collection: collection,
initialUri: initialUri, initialUri: initialUri,
), ),
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
@ -74,12 +75,12 @@ class FullscreenPage extends StatelessWidget {
} }
class FullscreenBody extends StatefulWidget { class FullscreenBody extends StatefulWidget {
final List<ImageEntry> entries; final ImageCollection collection;
final String initialUri; final String initialUri;
const FullscreenBody({ const FullscreenBody({
Key key, Key key,
this.entries, this.collection,
this.initialUri, this.initialUri,
}) : super(key: key); }) : super(key: key);
@ -97,7 +98,9 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
Animation<Offset> _bottomOverlayOffset; Animation<Offset> _bottomOverlayOffset;
EdgeInsets _frozenViewInsets, _frozenViewPadding; EdgeInsets _frozenViewInsets, _frozenViewPadding;
List<ImageEntry> get entries => widget.entries; ImageCollection get collection => widget.collection;
List<ImageEntry> get entries => widget.collection.entries;
@override @override
void initState() { void initState() {
@ -149,7 +152,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
onPageChanged: (page) => setState(() => _currentVerticalPage = page), onPageChanged: (page) => setState(() => _currentVerticalPage = page),
children: [ children: [
ImagePage( ImagePage(
entries: entries, collection: collection,
pageController: _horizontalPager, pageController: _horizontalPager,
onTap: () => _overlayVisible.value = !_overlayVisible.value, onTap: () => _overlayVisible.value = !_overlayVisible.value,
onPageChanged: (page) => setState(() => _currentHorizontalPage = page), onPageChanged: (page) => setState(() => _currentHorizontalPage = page),
@ -286,10 +289,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
}, },
); );
if (confirmed == null || !confirmed) return; if (confirmed == null || !confirmed) return;
if (await entry.delete()) if (!await collection.delete(entry)) showFeedback('Failed');
entries.remove(entry);
else
showFeedback('Failed');
} }
showRenameDialog(ImageEntry entry) async { showRenameDialog(ImageEntry entry) async {
@ -323,14 +323,14 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
enum FullscreenAction { delete, edit, info, open, openMap, rename, rotateCCW, rotateCW, setAs, share } enum FullscreenAction { delete, edit, info, open, openMap, rename, rotateCCW, rotateCW, setAs, share }
class ImagePage extends StatefulWidget { class ImagePage extends StatefulWidget {
final List<ImageEntry> entries; final ImageCollection collection;
final PageController pageController; final PageController pageController;
final VoidCallback onTap; final VoidCallback onTap;
final ValueChanged<int> onPageChanged; final ValueChanged<int> onPageChanged;
final ValueChanged<PhotoViewScaleState> onScaleChanged; final ValueChanged<PhotoViewScaleState> onScaleChanged;
const ImagePage({ const ImagePage({
this.entries, this.collection,
this.pageController, this.pageController,
this.onTap, this.onTap,
this.onPageChanged, this.onPageChanged,
@ -342,13 +342,15 @@ class ImagePage extends StatefulWidget {
} }
class ImagePageState extends State<ImagePage> with AutomaticKeepAliveClientMixin { class ImagePageState extends State<ImagePage> with AutomaticKeepAliveClientMixin {
List<ImageEntry> get entries => widget.collection.entries;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
return PhotoViewGallery.builder( return PhotoViewGallery.builder(
itemCount: widget.entries.length, itemCount: entries.length,
builder: (galleryContext, index) { builder: (galleryContext, index) {
final entry = widget.entries[index]; final entry = entries[index];
if (entry.isVideo) { if (entry.isVideo) {
return PhotoViewGalleryPageOptions.customChild( return PhotoViewGalleryPageOptions.customChild(
child: AvesVideo(entry: entry), child: AvesVideo(entry: entry),