collection: added group & sort options

This commit is contained in:
Thibault Deckers 2019-08-28 23:43:37 +09:00
parent 5bb2e914c6
commit 98def189dc
11 changed files with 152 additions and 63 deletions

View file

@ -37,7 +37,11 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> {
static const EventChannel eventChannel = EventChannel('deckers.thibault/aves/mediastore');
ImageCollection localMediaCollection = ImageCollection(List());
ImageCollection localMediaCollection = ImageCollection(
entries: List(),
groupFactor: settings.collectionGroupFactor,
sortFactor: settings.collectionSortFactor,
);
@override
void initState() {
@ -56,7 +60,7 @@ class _HomePageState extends State<HomePage> {
await metadataDb.init();
eventChannel.receiveBroadcastStream().cast<Map>().listen(
(entryMap) => localMediaCollection.entries.add(ImageEntry.fromMap(entryMap)),
(entryMap) => localMediaCollection.add(ImageEntry.fromMap(entryMap)),
onDone: () async {
debugPrint('mediastore stream done');
await localMediaCollection.loadCatalogMetadata();

View file

@ -6,31 +6,62 @@ import "package:collection/collection.dart";
import 'package:flutter/material.dart';
class ImageCollection with ChangeNotifier {
final List<ImageEntry> entries;
final List<ImageEntry> _rawEntries;
GroupFactor groupFactor = GroupFactor.date;
SortFactor sortFactor = SortFactor.date;
ImageCollection(this.entries);
ImageCollection({
@required List<ImageEntry> entries,
@required this.groupFactor,
@required this.sortFactor,
}) : _rawEntries = entries;
Map<dynamic, List<ImageEntry>> get sections {
switch (groupFactor) {
case GroupFactor.album:
return groupBy(entries, (entry) => entry.bucketDisplayName);
case GroupFactor.date:
return groupBy(entries, (entry) => entry.monthTaken);
switch (sortFactor) {
case SortFactor.date:
switch (groupFactor) {
case GroupFactor.album:
return groupBy(_rawEntries, (entry) => entry.bucketDisplayName);
case GroupFactor.date:
return groupBy(_rawEntries, (entry) => entry.monthTaken);
}
break;
case SortFactor.size:
return Map.fromEntries([MapEntry('All', _rawEntries)]);
}
return Map();
}
List<ImageEntry> get sortedEntries {
return List.unmodifiable(sections.entries.expand((e) => e.value));
}
group(GroupFactor groupFactor) {
this.groupFactor = groupFactor;
notifyListeners();
}
sort(SortFactor sortFactor) {
this.sortFactor = sortFactor;
switch (sortFactor) {
case SortFactor.date:
_rawEntries.sort((a, b) => b.bestDate.compareTo(a.bestDate));
break;
case SortFactor.size:
_rawEntries.sort((a, b) => b.sizeBytes.compareTo(a.sizeBytes));
break;
}
notifyListeners();
}
add(ImageEntry entry) => _rawEntries.add(entry);
Future<bool> delete(ImageEntry entry) async {
final success = await ImageFileService.delete(entry);
if (success) {
entries.remove(entry);
_rawEntries.remove(entry);
notifyListeners();
}
return success;
@ -40,7 +71,7 @@ class ImageCollection with ChangeNotifier {
debugPrint('$runtimeType loadCatalogMetadata start');
final start = DateTime.now();
final saved = await metadataDb.loadMetadataEntries();
entries.forEach((entry) {
_rawEntries.forEach((entry) {
final contentId = entry.contentId;
if (contentId != null) {
entry.catalogMetadata = saved.firstWhere((metadata) => metadata.contentId == contentId, orElse: () => null);
@ -53,7 +84,7 @@ class ImageCollection with ChangeNotifier {
debugPrint('$runtimeType loadAddresses start');
final start = DateTime.now();
final saved = await metadataDb.loadAddresses();
entries.forEach((entry) {
_rawEntries.forEach((entry) {
final contentId = entry.contentId;
if (contentId != null) {
entry.addressDetails = saved.firstWhere((address) => address.contentId == contentId, orElse: () => null);
@ -65,24 +96,23 @@ class ImageCollection with ChangeNotifier {
catalogEntries() async {
debugPrint('$runtimeType catalogEntries start');
final start = DateTime.now();
final uncataloguedEntries = entries.where((entry) => !entry.isCatalogued);
final uncataloguedEntries = _rawEntries.where((entry) => !entry.isCatalogued);
final newMetadata = List<CatalogMetadata>();
await Future.forEach<ImageEntry>(uncataloguedEntries, (entry) async {
await entry.catalog();
newMetadata.add(entry.catalogMetadata);
});
metadataDb.saveMetadata(List.unmodifiable(newMetadata));
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));
// notify because metadata dates might change groups and order
notifyListeners();
}
locateEntries() async {
debugPrint('$runtimeType locateEntries start');
final start = DateTime.now();
final unlocatedEntries = entries.where((entry) => !entry.isLocated);
final unlocatedEntries = _rawEntries.where((entry) => entry.hasGps && !entry.isLocated);
final newAddresses = List<AddressDetails>();
await Future.forEach<ImageEntry>(unlocatedEntries, (entry) async {
await entry.locate();
@ -92,8 +122,11 @@ class ImageCollection with ChangeNotifier {
newAddresses.clear();
}
});
metadataDb.saveAddresses(List.unmodifiable(newAddresses));
debugPrint('$runtimeType locateEntries complete in ${DateTime.now().difference(start).inSeconds}s');
}
}
enum SortFactor { date, size }
enum GroupFactor { album, date }

View file

@ -147,27 +147,28 @@ class ImageEntry with ChangeNotifier {
locate() async {
if (isLocated) return;
await catalog();
final latitude = catalogMetadata?.latitude;
final longitude = catalogMetadata?.longitude;
if (latitude != null && longitude != null) {
final coordinates = Coordinates(latitude, longitude);
try {
final addresses = await Geocoder.local.findAddressesFromCoordinates(coordinates);
if (addresses != null && addresses.length > 0) {
final address = addresses.first;
addressDetails = AddressDetails(
contentId: contentId,
addressLine: address.addressLine,
countryName: address.countryName,
adminArea: address.adminArea,
locality: address.locality,
);
notifyListeners();
}
} catch (e) {
debugPrint('$runtimeType addAddressToMetadata failed with exception=${e.message}');
if (latitude == null || longitude == null) return;
final coordinates = Coordinates(latitude, longitude);
try {
final addresses = await Geocoder.local.findAddressesFromCoordinates(coordinates);
if (addresses != null && addresses.length > 0) {
final address = addresses.first;
addressDetails = AddressDetails(
contentId: contentId,
addressLine: address.addressLine,
countryName: address.countryName,
adminArea: address.adminArea,
locality: address.locality,
);
notifyListeners();
}
} catch (e) {
debugPrint('$runtimeType addAddressToMetadata failed with exception=${e.message}');
}
}

View file

@ -16,7 +16,6 @@ class ImageFileService {
}
static Future<Uint8List> getImageBytes(ImageEntry entry, int width, int height) async {
// debugPrint('getImageBytes with path=${entry.path} contentId=${entry.contentId}');
if (width > 0 && height > 0) {
try {
final result = await platform.invokeMethod('getImageBytes', <String, dynamic>{
@ -32,16 +31,6 @@ class ImageFileService {
return Uint8List(0);
}
static cancelGetImageBytes(String uri) async {
try {
await platform.invokeMethod('cancelGetImageBytes', <String, dynamic>{
'uri': uri,
});
} on PlatformException catch (e) {
debugPrint('cancelGetImageBytes failed with exception=${e.message}');
}
}
static Future<bool> delete(ImageEntry entry) async {
try {
await platform.invokeMethod('delete', <String, dynamic>{

View file

@ -1,3 +1,4 @@
import 'package:aves/model/image_collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -14,6 +15,8 @@ class Settings {
Settings._private();
// preferences
static const collectionGroupFactorKey = 'collection_group_factor';
static const collectionSortFactorKey = 'collection_sort_factor';
static const infoMapZoomKey = 'info_map_zoom';
init() async {
@ -44,10 +47,28 @@ class Settings {
set infoMapZoom(double newValue) => setAndNotify(infoMapZoomKey, newValue);
GroupFactor get collectionGroupFactor => getEnumOrDefault(collectionGroupFactorKey, GroupFactor.date, GroupFactor.values);
set collectionGroupFactor(GroupFactor newValue) => setAndNotify(collectionGroupFactorKey, newValue.toString());
SortFactor get collectionSortFactor => getEnumOrDefault(collectionSortFactorKey, SortFactor.date, SortFactor.values);
set collectionSortFactor(SortFactor newValue) => setAndNotify(collectionSortFactorKey, newValue.toString());
// convenience methods
bool getBoolOrDefault(String key, bool defaultValue) => prefs.getKeys().contains(key) ? prefs.getBool(key) : defaultValue;
T getEnumOrDefault<T>(String key, T defaultValue, List<T> values) {
final valueString = prefs.getString(key);
for (T element in values) {
if (element.toString() == valueString) {
return element;
}
}
return defaultValue;
}
setAndNotify(String key, dynamic newValue) {
var oldValue = prefs.get(key);
if (newValue == null) {

View file

@ -1,4 +1,5 @@
import 'package:aves/model/image_collection.dart';
import 'package:aves/model/settings.dart';
import 'package:aves/widgets/album/search_delegate.dart';
import 'package:aves/widgets/album/thumbnail_collection.dart';
import 'package:aves/widgets/common/menu_row.dart';
@ -27,16 +28,27 @@ class AllCollectionPage extends StatelessWidget {
PopupMenuButton<AlbumAction>(
itemBuilder: (context) => [
PopupMenuItem(
value: AlbumAction.groupByAlbum,
child: MenuRow(text: 'Group by album', checked: collection.groupFactor == GroupFactor.album),
value: AlbumAction.sortByDate,
child: MenuRow(text: 'Sort by date', checked: collection.sortFactor == SortFactor.date),
),
PopupMenuItem(
value: AlbumAction.groupByDate,
child: MenuRow(text: 'Group by date', checked: collection.groupFactor == GroupFactor.date),
value: AlbumAction.sortBySize,
child: MenuRow(text: 'Sort by size', checked: collection.sortFactor == SortFactor.size),
),
PopupMenuDivider(),
if (collection.sortFactor == SortFactor.date) ...[
PopupMenuItem(
value: AlbumAction.groupByAlbum,
child: MenuRow(text: 'Group by album', checked: collection.groupFactor == GroupFactor.album),
),
PopupMenuItem(
value: AlbumAction.groupByDate,
child: MenuRow(text: 'Group by date', checked: collection.groupFactor == GroupFactor.date),
),
PopupMenuDivider(),
],
PopupMenuItem(
value: AlbumAction.groupByAlbum,
value: AlbumAction.debug,
child: MenuRow(text: 'Debug', icon: Icons.whatshot),
),
],
@ -50,14 +62,22 @@ class AllCollectionPage extends StatelessWidget {
onActionSelected(BuildContext context, AlbumAction action) {
switch (action) {
case AlbumAction.debug:
goToDebug(context);
break;
case AlbumAction.groupByAlbum:
collection.group(GroupFactor.album);
break;
case AlbumAction.groupByDate:
collection.group(GroupFactor.date);
break;
case AlbumAction.debug:
goToDebug(context);
case AlbumAction.sortByDate:
settings.collectionSortFactor = SortFactor.date;
collection.sort(SortFactor.date);
break;
case AlbumAction.sortBySize:
settings.collectionSortFactor = SortFactor.size;
collection.sort(SortFactor.size);
break;
}
}
@ -67,11 +87,11 @@ class AllCollectionPage extends StatelessWidget {
context,
MaterialPageRoute(
builder: (context) => DebugPage(
entries: collection.entries,
entries: collection.sortedEntries,
),
),
);
}
}
enum AlbumAction { groupByDate, groupByAlbum, debug }
enum AlbumAction { debug, groupByAlbum, groupByDate, sortByDate, sortBySize }

View file

@ -52,7 +52,7 @@ class ImageSearchDelegate extends SearchDelegate<ImageEntry> {
return SizedBox.shrink();
}
final lowerQuery = query.toLowerCase();
final matches = collection.entries.where((entry) => entry.search(lowerQuery)).toList();
final matches = collection.sortedEntries.where((entry) => entry.search(lowerQuery)).toList();
if (matches.isEmpty) {
return Center(
child: Text(
@ -61,6 +61,12 @@ class ImageSearchDelegate extends SearchDelegate<ImageEntry> {
),
);
}
return ThumbnailCollection(collection: ImageCollection(matches));
return ThumbnailCollection(
collection: ImageCollection(
entries: matches,
groupFactor: collection.groupFactor,
sortFactor: collection.sortFactor,
),
);
}
}

View file

@ -54,7 +54,6 @@ class ThumbnailState extends State<Thumbnail> {
@override
void dispose() {
entry.removeListener(onEntryChange);
ImageFileService.cancelGetImageBytes(uri);
super.dispose();
}

View file

@ -91,10 +91,20 @@ class SectionSliver extends StatelessWidget {
@override
Widget build(BuildContext context) {
// debugPrint('$runtimeType build with sectionKey=$sectionKey');
final columnCount = 4;
Widget header = SizedBox.shrink();
if (collection.sortFactor == SortFactor.date) {
switch (collection.groupFactor) {
case GroupFactor.album:
header = SectionHeader(text: sectionKey);
break;
case GroupFactor.date:
header = MonthSectionHeader(date: sectionKey);
break;
}
}
return SliverStickyHeader(
header: collection.groupFactor == GroupFactor.date ? MonthSectionHeader(date: sectionKey) : SectionHeader(text: sectionKey),
header: header,
sliver: SliverGrid(
delegate: SliverChildBuilderDelegate(
(sliverContext, index) {

View file

@ -1,6 +1,7 @@
import 'package:aves/model/image_entry.dart';
import 'package:aves/model/image_metadata.dart';
import 'package:aves/model/metadata_db.dart';
import 'package:aves/model/settings.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
@ -39,6 +40,11 @@ class DebugPageState extends State<DebugPage> {
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Settings'),
Text('collectionGroupFactor: ${settings.collectionGroupFactor}'),
Text('collectionSortFactor: ${settings.collectionSortFactor}'),
Text('infoMapZoom: ${settings.infoMapZoom}'),
Divider(),
Text('Entries: ${entries.length}'),
...byMimeTypes.keys.map((mimeType) => Text('- $mimeType: ${byMimeTypes[mimeType].length}')),
Text('Catalogued: ${catalogued.length}'),

View file

@ -100,7 +100,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
ImageCollection get collection => widget.collection;
List<ImageEntry> get entries => widget.collection.entries;
List<ImageEntry> get entries => widget.collection.sortedEntries;
@override
void initState() {
@ -342,7 +342,7 @@ class ImagePage extends StatefulWidget {
}
class ImagePageState extends State<ImagePage> with AutomaticKeepAliveClientMixin {
List<ImageEntry> get entries => widget.collection.entries;
List<ImageEntry> get entries => widget.collection.sortedEntries;
@override
Widget build(BuildContext context) {