model: added collection
This commit is contained in:
parent
7d2a27f797
commit
be66415842
7 changed files with 148 additions and 104 deletions
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
79
lib/model/image_collection.dart
Normal file
79
lib/model/image_collection.dart
Normal 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');
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Reference in a new issue