diff --git a/lib/main.dart b/lib/main.dart index 88759d07a..208f53f77 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -37,7 +37,11 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { 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 { await metadataDb.init(); eventChannel.receiveBroadcastStream().cast().listen( - (entryMap) => localMediaCollection.entries.add(ImageEntry.fromMap(entryMap)), + (entryMap) => localMediaCollection.add(ImageEntry.fromMap(entryMap)), onDone: () async { debugPrint('mediastore stream done'); await localMediaCollection.loadCatalogMetadata(); diff --git a/lib/model/image_collection.dart b/lib/model/image_collection.dart index 87faafa33..3e2bad9ff 100644 --- a/lib/model/image_collection.dart +++ b/lib/model/image_collection.dart @@ -6,31 +6,62 @@ import "package:collection/collection.dart"; import 'package:flutter/material.dart'; class ImageCollection with ChangeNotifier { - final List entries; - + final List _rawEntries; GroupFactor groupFactor = GroupFactor.date; + SortFactor sortFactor = SortFactor.date; - ImageCollection(this.entries); + ImageCollection({ + @required List entries, + @required this.groupFactor, + @required this.sortFactor, + }) : _rawEntries = entries; Map> 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 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 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(); await Future.forEach(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(); await Future.forEach(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 } diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index eca6e4a73..cbcaaf58d 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -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}'); } } diff --git a/lib/model/image_file_service.dart b/lib/model/image_file_service.dart index ef47f4f03..3c8e995d2 100644 --- a/lib/model/image_file_service.dart +++ b/lib/model/image_file_service.dart @@ -16,7 +16,6 @@ class ImageFileService { } static Future 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', { @@ -32,16 +31,6 @@ class ImageFileService { return Uint8List(0); } - static cancelGetImageBytes(String uri) async { - try { - await platform.invokeMethod('cancelGetImageBytes', { - 'uri': uri, - }); - } on PlatformException catch (e) { - debugPrint('cancelGetImageBytes failed with exception=${e.message}'); - } - } - static Future delete(ImageEntry entry) async { try { await platform.invokeMethod('delete', { diff --git a/lib/model/settings.dart b/lib/model/settings.dart index 66329fa13..e891bdee6 100644 --- a/lib/model/settings.dart +++ b/lib/model/settings.dart @@ -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(String key, T defaultValue, List 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) { diff --git a/lib/widgets/album/all_collection_page.dart b/lib/widgets/album/all_collection_page.dart index 820650c37..8bfad8be7 100644 --- a/lib/widgets/album/all_collection_page.dart +++ b/lib/widgets/album/all_collection_page.dart @@ -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( 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 } diff --git a/lib/widgets/album/search_delegate.dart b/lib/widgets/album/search_delegate.dart index 43c8df23d..227a90048 100644 --- a/lib/widgets/album/search_delegate.dart +++ b/lib/widgets/album/search_delegate.dart @@ -52,7 +52,7 @@ class ImageSearchDelegate extends SearchDelegate { 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 { ), ); } - return ThumbnailCollection(collection: ImageCollection(matches)); + return ThumbnailCollection( + collection: ImageCollection( + entries: matches, + groupFactor: collection.groupFactor, + sortFactor: collection.sortFactor, + ), + ); } } diff --git a/lib/widgets/album/thumbnail.dart b/lib/widgets/album/thumbnail.dart index f6b1c1a27..5a32ddfe8 100644 --- a/lib/widgets/album/thumbnail.dart +++ b/lib/widgets/album/thumbnail.dart @@ -54,7 +54,6 @@ class ThumbnailState extends State { @override void dispose() { entry.removeListener(onEntryChange); - ImageFileService.cancelGetImageBytes(uri); super.dispose(); } diff --git a/lib/widgets/album/thumbnail_collection.dart b/lib/widgets/album/thumbnail_collection.dart index 03ed8e20b..1808fda3b 100644 --- a/lib/widgets/album/thumbnail_collection.dart +++ b/lib/widgets/album/thumbnail_collection.dart @@ -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) { diff --git a/lib/widgets/debug_page.dart b/lib/widgets/debug_page.dart index 75eaa8382..52b030848 100644 --- a/lib/widgets/debug_page.dart +++ b/lib/widgets/debug_page.dart @@ -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 { 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}'), diff --git a/lib/widgets/fullscreen/image_page.dart b/lib/widgets/fullscreen/image_page.dart index c0269e687..df0f8e768 100644 --- a/lib/widgets/fullscreen/image_page.dart +++ b/lib/widgets/fullscreen/image_page.dart @@ -100,7 +100,7 @@ class FullscreenBodyState extends State with SingleTickerProvide ImageCollection get collection => widget.collection; - List get entries => widget.collection.entries; + List get entries => widget.collection.sortedEntries; @override void initState() { @@ -342,7 +342,7 @@ class ImagePage extends StatefulWidget { } class ImagePageState extends State with AutomaticKeepAliveClientMixin { - List get entries => widget.collection.entries; + List get entries => widget.collection.sortedEntries; @override Widget build(BuildContext context) {