collection: added group & sort options
This commit is contained in:
parent
5bb2e914c6
commit
98def189dc
11 changed files with 152 additions and 63 deletions
|
@ -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();
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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}');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>{
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,7 +54,6 @@ class ThumbnailState extends State<Thumbnail> {
|
|||
@override
|
||||
void dispose() {
|
||||
entry.removeListener(onEntryChange);
|
||||
ImageFileService.cancelGetImageBytes(uri);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}'),
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue