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_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<HomePage> {
static const EventChannel eventChannel = EventChannel('deckers.thibault/aves/mediastore');
List<ImageEntry> entries = List();
ImageCollection localMediaCollection = ImageCollection(List());
@override
void initState() {
@ -47,15 +47,15 @@ class _HomePageState extends State<HomePage> {
await metadataDb.init();
eventChannel.receiveBroadcastStream().cast<Map>().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<HomePage> {
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<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;
}
Future<bool> delete() => ImageFileService.delete(this);
Future<bool> rename(String newName) async {
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/thumbnail_collection.dart';
import 'package:aves/widgets/debug_page.dart';
import 'package:flutter/material.dart';
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
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,
),
),
);

View file

@ -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<ImageEntry> {
final List<ImageEntry> entries;
final ImageCollection collection;
ImageSearchDelegate(this.entries);
ImageSearchDelegate(this.collection);
@override
ThemeData appBarTheme(BuildContext context) {
@ -51,7 +52,7 @@ class ImageSearchDelegate extends SearchDelegate<ImageEntry> {
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<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/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<ImageEntry> 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<DateTime, List<ImageEntry>> _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<ImageEntry> entries;
final ImageCollection collection;
final Map<DateTime, List<ImageEntry>> 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,
),
),

View file

@ -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<ImageEntry> 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<ImageEntry> 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<FullscreenBody> with SingleTickerProvide
Animation<Offset> _bottomOverlayOffset;
EdgeInsets _frozenViewInsets, _frozenViewPadding;
List<ImageEntry> get entries => widget.entries;
ImageCollection get collection => widget.collection;
List<ImageEntry> get entries => widget.collection.entries;
@override
void initState() {
@ -149,7 +152,7 @@ class FullscreenBodyState extends State<FullscreenBody> 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<FullscreenBody> 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<FullscreenBody> with SingleTickerProvide
enum FullscreenAction { delete, edit, info, open, openMap, rename, rotateCCW, rotateCW, setAs, share }
class ImagePage extends StatefulWidget {
final List<ImageEntry> entries;
final ImageCollection collection;
final PageController pageController;
final VoidCallback onTap;
final ValueChanged<int> onPageChanged;
final ValueChanged<PhotoViewScaleState> 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<ImagePage> with AutomaticKeepAliveClientMixin {
List<ImageEntry> 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),