diff --git a/lib/labs/sliver_transition_grid_delegate.dart b/lib/labs/sliver_transition_grid_delegate.dart index 90ea0d1be..fb9852d68 100644 --- a/lib/labs/sliver_transition_grid_delegate.dart +++ b/lib/labs/sliver_transition_grid_delegate.dart @@ -164,7 +164,7 @@ class SliverTransitionGridTileLayout extends SliverGridLayout { if (t != 0) { final index = childCount - 1; - var maxScrollOffset = lerpDouble(_getScrollOffset(index, floor), _getScrollOffset(index, ceil), t) + current.mainAxisStride; + final maxScrollOffset = lerpDouble(_getScrollOffset(index, floor), _getScrollOffset(index, ceil), t) + current.mainAxisStride; return maxScrollOffset; } diff --git a/lib/model/collection_filters.dart b/lib/model/collection_filters.dart new file mode 100644 index 000000000..d334d615e --- /dev/null +++ b/lib/model/collection_filters.dart @@ -0,0 +1,39 @@ +import 'package:aves/model/image_entry.dart'; + +abstract class CollectionFilter { + const CollectionFilter(); + + bool filter(ImageEntry entry); +} + +class AlbumFilter extends CollectionFilter { + final String album; + + const AlbumFilter(this.album); + + @override + bool filter(ImageEntry entry) => entry.directory == album; +} + +class TagFilter extends CollectionFilter { + final String tag; + + const TagFilter(this.tag); + + @override + bool filter(ImageEntry entry) => entry.xmpSubjects.contains(tag); +} + +class VideoFilter extends CollectionFilter { + @override + bool filter(ImageEntry entry) => entry.isVideo; +} + +class MetadataFilter extends CollectionFilter { + final String value; + + const MetadataFilter(this.value); + + @override + bool filter(ImageEntry entry) => entry.search(value); +} diff --git a/lib/model/collection_lens.dart b/lib/model/collection_lens.dart new file mode 100644 index 000000000..376141158 --- /dev/null +++ b/lib/model/collection_lens.dart @@ -0,0 +1,157 @@ +import 'dart:async'; +import 'dart:collection'; + +import 'package:aves/model/collection_filters.dart'; +import 'package:aves/model/collection_source.dart'; +import 'package:aves/model/image_entry.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; + +class CollectionLens with ChangeNotifier { + final CollectionSource source; + final List filters; + GroupFactor groupFactor; + SortFactor sortFactor; + + List _filteredEntries; + List _subscriptions = []; + + Map> sections = Map.unmodifiable({}); + + CollectionLens({ + @required this.source, + List filters, + GroupFactor groupFactor, + SortFactor sortFactor, + }) : this.filters = filters ?? [], + this.groupFactor = groupFactor ?? GroupFactor.month, + this.sortFactor = sortFactor ?? SortFactor.date { + _subscriptions.add(source.eventBus.on().listen((e) => onSourceChanged())); + _subscriptions.add(source.eventBus.on().listen((e) => onSourceChanged())); + _subscriptions.add(source.eventBus.on().listen((e) => onMetadataChanged())); + onSourceChanged(); + } + + factory CollectionLens.empty() { + return CollectionLens( + source: CollectionSource(), + ); + } + + factory CollectionLens.from(CollectionLens lens, CollectionFilter filter) { + return CollectionLens( + source: lens.source, + filters: [...lens.filters, filter], + groupFactor: lens.groupFactor, + sortFactor: lens.sortFactor, + ); + } + + @override + void dispose() { + _subscriptions + ..forEach((sub) => sub.cancel()) + ..clear(); + _subscriptions = null; + super.dispose(); + } + + bool get isEmpty => _filteredEntries.isEmpty; + + int get imageCount => _filteredEntries.where((entry) => !entry.isVideo).length; + + int get videoCount => _filteredEntries.where((entry) => entry.isVideo).length; + + List get sortedEntries => List.unmodifiable(sections.entries.expand((e) => e.value)); + + void sort(SortFactor sortFactor) { + this.sortFactor = sortFactor; + updateSections(); + } + + void group(GroupFactor groupFactor) { + this.groupFactor = groupFactor; + updateSections(); + } + + void updateSections() { + _applySort(); + switch (sortFactor) { + case SortFactor.date: + switch (groupFactor) { + case GroupFactor.album: + sections = Map.unmodifiable(groupBy(_filteredEntries, (entry) => entry.directory)); + break; + case GroupFactor.month: + sections = Map.unmodifiable(groupBy(_filteredEntries, (entry) => entry.monthTaken)); + break; + case GroupFactor.day: + sections = Map.unmodifiable(groupBy(_filteredEntries, (entry) => entry.dayTaken)); + break; + } + break; + case SortFactor.size: + sections = Map.unmodifiable(Map.fromEntries([ + MapEntry(null, _filteredEntries), + ])); + break; + case SortFactor.name: + final byAlbum = groupBy(_filteredEntries, (ImageEntry entry) => entry.directory); + final albums = byAlbum.keys.toSet(); + final compare = (a, b) { + final ua = CollectionSource.getUniqueAlbumName(a, albums); + final ub = CollectionSource.getUniqueAlbumName(b, albums); + return compareAsciiUpperCase(ua, ub); + }; + sections = Map.unmodifiable(SplayTreeMap.from(byAlbum, compare)); + break; + } + notifyListeners(); + } + + void _applySort() { + switch (sortFactor) { + case SortFactor.date: + _filteredEntries.sort((a, b) => b.bestDate.compareTo(a.bestDate)); + break; + case SortFactor.size: + _filteredEntries.sort((a, b) => b.sizeBytes.compareTo(a.sizeBytes)); + break; + case SortFactor.name: + _filteredEntries.sort((a, b) => compareAsciiUpperCase(a.title, b.title)); + break; + } + } + +// void add(ImageEntry entry) => _rawEntries.add(entry); +// +// Future delete(ImageEntry entry) async { +// final success = await ImageFileService.delete(entry); +// if (success) { +// _rawEntries.remove(entry); +// updateSections(); +// } +// return success; +// } + + void onSourceChanged() { + _applyFilters(); + updateSections(); + } + + void _applyFilters() { + final rawEntries = source.entries; + _filteredEntries = List.of(filters.isEmpty ? rawEntries : rawEntries.where((entry) => filters.fold(true, (prev, filter) => prev && filter.filter(entry)))); + updateSections(); + } + + void onMetadataChanged() { + _applyFilters(); + // metadata dates impact sorting and grouping + updateSections(); + } +} + +enum SortFactor { date, size, name } + +enum GroupFactor { album, month, day } diff --git a/lib/model/image_collection.dart b/lib/model/collection_source.dart similarity index 59% rename from lib/model/image_collection.dart rename to lib/model/collection_source.dart index c74a09471..9ff1c6ae4 100644 --- a/lib/model/image_collection.dart +++ b/lib/model/collection_source.dart @@ -1,131 +1,30 @@ -import 'dart:collection'; - 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:collection/collection.dart'; +import 'package:event_bus/event_bus.dart'; import 'package:flutter/foundation.dart'; import 'package:path/path.dart'; -class ImageCollection with ChangeNotifier { +class CollectionSource { final List _rawEntries; - Map> sections = Map.unmodifiable({}); - GroupFactor groupFactor = GroupFactor.month; - SortFactor sortFactor = SortFactor.date; + final EventBus _eventBus = EventBus(); + List sortedAlbums = List.unmodifiable(const Iterable.empty()); List sortedTags = List.unmodifiable(const Iterable.empty()); - ImageCollection({ - @required List entries, - this.groupFactor, - this.sortFactor, - }) : _rawEntries = entries { - if (_rawEntries.isNotEmpty) updateSections(); - } + List get entries => List.unmodifiable(_rawEntries); - int get imageCount => _rawEntries.where((entry) => !entry.isVideo).length; - - int get videoCount => _rawEntries.where((entry) => entry.isVideo).length; + EventBus get eventBus => _eventBus; int get albumCount => sortedAlbums.length; int get tagCount => sortedTags.length; - List get sortedEntries => List.unmodifiable(sections.entries.expand((e) => e.value)); - - void sort(SortFactor sortFactor) { - this.sortFactor = sortFactor; - updateSections(); - } - - void group(GroupFactor groupFactor) { - this.groupFactor = groupFactor; - updateSections(); - } - - void updateSections() { - _applySort(); - switch (sortFactor) { - case SortFactor.date: - switch (groupFactor) { - case GroupFactor.album: - sections = Map.unmodifiable(groupBy(_rawEntries, (entry) => entry.directory)); - break; - case GroupFactor.month: - sections = Map.unmodifiable(groupBy(_rawEntries, (entry) => entry.monthTaken)); - break; - case GroupFactor.day: - sections = Map.unmodifiable(groupBy(_rawEntries, (entry) => entry.dayTaken)); - break; - } - break; - case SortFactor.size: - sections = Map.unmodifiable(Map.fromEntries([ - MapEntry(null, _rawEntries), - ])); - break; - case SortFactor.name: - final byAlbum = groupBy(_rawEntries, (ImageEntry entry) => entry.directory); - final albums = byAlbum.keys.toSet(); - final compare = (a, b) { - final ua = getUniqueAlbumName(a, albums); - final ub = getUniqueAlbumName(b, albums); - return compareAsciiUpperCase(ua, ub); - }; - sections = Map.unmodifiable(SplayTreeMap.from(byAlbum, compare)); - break; - } - notifyListeners(); - } - - void _applySort() { - 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; - case SortFactor.name: - _rawEntries.sort((a, b) => compareAsciiUpperCase(a.title, b.title)); - break; - } - } - - void add(ImageEntry entry) => _rawEntries.add(entry); - - Future delete(ImageEntry entry) async { - final success = await ImageFileService.delete(entry); - if (success) { - _rawEntries.remove(entry); - updateSections(); - } - return success; - } - - void updateAlbums() { - final albums = _rawEntries.map((entry) => entry.directory).toSet(); - final sorted = albums.toList() - ..sort((a, b) { - final ua = getUniqueAlbumName(a, albums); - final ub = getUniqueAlbumName(b, albums); - return compareAsciiUpperCase(ua, ub); - }); - sortedAlbums = List.unmodifiable(sorted); - } - - void updateTags() { - final tags = _rawEntries.expand((entry) => entry.xmpSubjects).toSet(); - final sorted = tags.toList()..sort(compareAsciiUpperCase); - sortedTags = List.unmodifiable(sorted); - } - - void onMetadataChanged() { - // metadata dates impact sorting and grouping - updateSections(); - updateTags(); - } + CollectionSource({ + List entries, + }) : _rawEntries = entries ?? []; Future loadCatalogMetadata() async { final stopwatch = Stopwatch()..start(); @@ -171,6 +70,11 @@ class ImageCollection with ChangeNotifier { debugPrint('$runtimeType catalogEntries complete in ${stopwatch.elapsed.inSeconds}s with ${newMetadata.length} new entries'); } + void onMetadataChanged() { + updateTags(); + eventBus.fire(MetadataChangedEvent()); + } + Future locateEntries() async { final stopwatch = Stopwatch()..start(); final unlocatedEntries = _rawEntries.where((entry) => entry.hasGps && !entry.isLocated).toList(); @@ -189,15 +93,43 @@ class ImageCollection with ChangeNotifier { debugPrint('$runtimeType locateEntries complete in ${stopwatch.elapsed.inMilliseconds}ms'); } - ImageCollection filter(bool Function(ImageEntry) filter) { - return ImageCollection( - entries: _rawEntries.where(filter).toList(), - groupFactor: groupFactor, - sortFactor: sortFactor, - ); + void updateAlbums() { + final albums = _rawEntries.map((entry) => entry.directory).toSet(); + final sorted = albums.toList() + ..sort((a, b) { + final ua = getUniqueAlbumName(a, albums); + final ub = getUniqueAlbumName(b, albums); + return compareAsciiUpperCase(ua, ub); + }); + sortedAlbums = List.unmodifiable(sorted); } - String getUniqueAlbumName(String album, Iterable albums) { + void updateTags() { + final tags = _rawEntries.expand((entry) => entry.xmpSubjects).toSet(); + final sorted = tags.toList()..sort(compareAsciiUpperCase); + sortedTags = List.unmodifiable(sorted); + } + + void add(ImageEntry entry) { + _rawEntries.add(entry); + eventBus.fire(EntryAddedEvent(entry)); + } + + void addAll(Iterable entries) { + _rawEntries.addAll(entries); + eventBus.fire(const EntryAddedEvent()); + } + + Future delete(ImageEntry entry) async { + final success = await ImageFileService.delete(entry); + if (success) { + _rawEntries.remove(entry); + eventBus.fire(EntryRemovedEvent(entry)); + } + return success; + } + + static String getUniqueAlbumName(String album, Iterable albums) { final otherAlbums = albums.where((item) => item != album); final parts = album.split(separator); int partCount = 0; @@ -209,6 +141,16 @@ class ImageCollection with ChangeNotifier { } } -enum SortFactor { date, size, name } +class MetadataChangedEvent {} -enum GroupFactor { album, month, day } +class EntryAddedEvent { + final ImageEntry entry; + + const EntryAddedEvent([this.entry]); +} + +class EntryRemovedEvent { + final ImageEntry entry; + + const EntryRemovedEvent(this.entry); +} diff --git a/lib/model/settings.dart b/lib/model/settings.dart index b52de626d..42bbf0e8c 100644 --- a/lib/model/settings.dart +++ b/lib/model/settings.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/image_collection.dart'; +import 'package:aves/model/collection_lens.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:shared_preferences/shared_preferences.dart'; diff --git a/lib/widgets/album/all_collection_drawer.dart b/lib/widgets/album/all_collection_drawer.dart index 990915b97..b8ebc6c5e 100644 --- a/lib/widgets/album/all_collection_drawer.dart +++ b/lib/widgets/album/all_collection_drawer.dart @@ -1,7 +1,8 @@ import 'dart:ui'; -import 'package:aves/model/image_collection.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/collection_filters.dart'; +import 'package:aves/model/collection_lens.dart'; +import 'package:aves/model/collection_source.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/color_utils.dart'; import 'package:aves/widgets/album/filtered_collection_page.dart'; @@ -16,10 +17,11 @@ class AllCollectionDrawer extends StatelessWidget { @override Widget build(BuildContext context) { - final collection = Provider.of(context); - final tags = collection.sortedTags; + final collection = Provider.of(context); + final source = collection.source; + final tags = source.sortedTags; final regularAlbums = [], appAlbums = [], specialAlbums = []; - for (var album in collection.sortedAlbums) { + for (var album in source.sortedAlbums) { switch (androidFileUtils.getAlbumType(album)) { case AlbumType.Default: regularAlbums.add(album); @@ -37,13 +39,13 @@ class AllCollectionDrawer extends StatelessWidget { collection: collection, leading: const Icon(OMIcons.videoLibrary), title: 'Videos', - filter: (entry) => entry.isVideo, + filter: VideoFilter(), ); final buildAlbumEntry = (album) => _FilteredCollectionNavTile( collection: collection, leading: IconUtils.getAlbumIcon(context, album) ?? const Icon(OMIcons.photoAlbum), - title: collection.getUniqueAlbumName(album, collection.sortedAlbums), - filter: (entry) => entry.directory == album, + title: CollectionSource.getUniqueAlbumName(album, source.sortedAlbums), + filter: AlbumFilter(album), ); final buildTagEntry = (tag) => _FilteredCollectionNavTile( collection: collection, @@ -52,7 +54,7 @@ class AllCollectionDrawer extends StatelessWidget { color: stringToColor(tag), ), title: tag, - filter: (entry) => entry.xmpSubjects.contains(tag), + filter: TagFilter(tag), ); return Drawer( @@ -109,12 +111,12 @@ class AllCollectionDrawer extends StatelessWidget { Row(children: [ const Icon(OMIcons.photoAlbum), const SizedBox(width: 4), - Text('${collection.albumCount}'), + Text('${source.albumCount}'), ]), Row(children: [ const Icon(OMIcons.label), const SizedBox(width: 4), - Text('${collection.tagCount}'), + Text('${source.tagCount}'), ]), ], ), @@ -147,10 +149,10 @@ class AllCollectionDrawer extends StatelessWidget { } class _FilteredCollectionNavTile extends StatelessWidget { - final ImageCollection collection; + final CollectionLens collection; final Widget leading; final String title; - final bool Function(ImageEntry) filter; + final CollectionFilter filter; const _FilteredCollectionNavTile({ @required this.collection, diff --git a/lib/widgets/album/all_collection_page.dart b/lib/widgets/album/all_collection_page.dart index 9d553a33f..58625f07f 100644 --- a/lib/widgets/album/all_collection_page.dart +++ b/lib/widgets/album/all_collection_page.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/image_collection.dart'; +import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/settings.dart'; import 'package:aves/widgets/album/search_delegate.dart'; import 'package:aves/widgets/album/thumbnail_collection.dart'; @@ -31,7 +31,7 @@ class _AllCollectionAppBar extends SliverAppBar { static List _buildActions() { return [ Builder( - builder: (context) => Consumer( + builder: (context) => Consumer( builder: (context, collection, child) => IconButton( icon: Icon(OMIcons.search), onPressed: () => showSearch( @@ -42,7 +42,7 @@ class _AllCollectionAppBar extends SliverAppBar { ), ), Builder( - builder: (context) => Consumer( + builder: (context) => Consumer( builder: (context, collection, child) => PopupMenuButton( itemBuilder: (context) => [ PopupMenuItem( @@ -85,7 +85,7 @@ class _AllCollectionAppBar extends SliverAppBar { ]; } - static void _onActionSelected(BuildContext context, ImageCollection collection, AlbumAction action) { + static void _onActionSelected(BuildContext context, CollectionLens collection, AlbumAction action) { switch (action) { case AlbumAction.debug: _goToDebug(context, collection); @@ -117,7 +117,7 @@ class _AllCollectionAppBar extends SliverAppBar { } } - static Future _goToDebug(BuildContext context, ImageCollection collection) { + static Future _goToDebug(BuildContext context, CollectionLens collection) { return Navigator.push( context, MaterialPageRoute( diff --git a/lib/widgets/album/collection_section.dart b/lib/widgets/album/collection_section.dart index 7345fe443..29dbe87af 100644 --- a/lib/widgets/album/collection_section.dart +++ b/lib/widgets/album/collection_section.dart @@ -1,4 +1,5 @@ -import 'package:aves/model/image_collection.dart'; +import 'package:aves/model/collection_lens.dart'; +import 'package:aves/model/collection_source.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/widgets/album/sections.dart'; import 'package:aves/widgets/album/thumbnail.dart'; @@ -10,7 +11,7 @@ import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:provider/provider.dart'; class SectionSliver extends StatelessWidget { - final ImageCollection collection; + final CollectionLens collection; final dynamic sectionKey; final int columnCount; @@ -91,7 +92,7 @@ class ThumbnailMetadata { } class SectionHeader extends StatelessWidget { - final ImageCollection collection; + final CollectionLens collection; final Map> sections; final dynamic sectionKey; @@ -143,7 +144,7 @@ class SectionHeader extends StatelessWidget { child: albumIcon, ); } - var title = collection.getUniqueAlbumName(sectionKey as String, sections.keys.cast()); + final title = CollectionSource.getUniqueAlbumName(sectionKey as String, sections.keys.cast()); return TitleSectionHeader( key: ValueKey(title), leading: albumIcon, diff --git a/lib/widgets/album/filtered_collection_page.dart b/lib/widgets/album/filtered_collection_page.dart index 15d7b7cf5..1202887ff 100644 --- a/lib/widgets/album/filtered_collection_page.dart +++ b/lib/widgets/album/filtered_collection_page.dart @@ -1,24 +1,24 @@ -import 'package:aves/model/image_collection.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/collection_filters.dart'; +import 'package:aves/model/collection_lens.dart'; import 'package:aves/widgets/album/thumbnail_collection.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class FilteredCollectionPage extends StatelessWidget { - final ImageCollection collection; - final bool Function(ImageEntry) filter; + final CollectionLens collection; + final CollectionFilter filter; final String title; - FilteredCollectionPage({Key key, ImageCollection collection, this.filter, this.title}) - : this.collection = collection.filter(filter), + FilteredCollectionPage({Key key, CollectionLens collection, this.filter, this.title}) + : this.collection = CollectionLens.from(collection, filter), super(key: key); @override Widget build(BuildContext context) { return MediaQueryDataProvider( child: Scaffold( - body: ChangeNotifierProvider.value( + body: ChangeNotifierProvider.value( value: collection, child: ThumbnailCollection( appBar: SliverAppBar( diff --git a/lib/widgets/album/search_delegate.dart b/lib/widgets/album/search_delegate.dart index d584fa495..6990b3f04 100644 --- a/lib/widgets/album/search_delegate.dart +++ b/lib/widgets/album/search_delegate.dart @@ -1,4 +1,5 @@ -import 'package:aves/model/image_collection.dart'; +import 'package:aves/model/collection_filters.dart'; +import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/widgets/album/thumbnail_collection.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; @@ -7,7 +8,7 @@ import 'package:outline_material_icons/outline_material_icons.dart'; import 'package:provider/provider.dart'; class ImageSearchDelegate extends SearchDelegate { - final ImageCollection collection; + final CollectionLens collection; ImageSearchDelegate(this.collection); @@ -54,19 +55,17 @@ class ImageSearchDelegate extends SearchDelegate { showSuggestions(context); return const SizedBox.shrink(); } - final lowerQuery = query.toLowerCase(); - final matches = collection.sortedEntries.where((entry) => entry.search(lowerQuery)).toList(); - if (matches.isEmpty) { - return _EmptyContent(); - } return MediaQueryDataProvider( - child: ChangeNotifierProvider.value( - value: ImageCollection( - entries: matches, + child: ChangeNotifierProvider.value( + value: CollectionLens( + source: collection.source, + filters: [MetadataFilter(query.toLowerCase())], groupFactor: collection.groupFactor, sortFactor: collection.sortFactor, ), - child: ThumbnailCollection(), + child: ThumbnailCollection( + emptyBuilder: (context) => _EmptyContent(), + ), ), ); } @@ -76,7 +75,8 @@ class _EmptyContent extends StatelessWidget { @override Widget build(BuildContext context) { const color = Color(0xFF607D8B); - return Center( + return Align( + alignment: const FractionalOffset(.5, .4), child: Column( mainAxisSize: MainAxisSize.min, children: const [ diff --git a/lib/widgets/album/thumbnail.dart b/lib/widgets/album/thumbnail.dart index e4fece9b6..444763481 100644 --- a/lib/widgets/album/thumbnail.dart +++ b/lib/widgets/album/thumbnail.dart @@ -10,7 +10,7 @@ class Thumbnail extends StatelessWidget { final double extent; static final Color borderColor = Colors.grey.shade700; - static const double borderWidth = .5; + static const double borderWidth = .5; const Thumbnail({ Key key, diff --git a/lib/widgets/album/thumbnail_collection.dart b/lib/widgets/album/thumbnail_collection.dart index 1fba53642..bc2d01512 100644 --- a/lib/widgets/album/thumbnail_collection.dart +++ b/lib/widgets/album/thumbnail_collection.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/image_collection.dart'; +import 'package:aves/model/collection_lens.dart'; import 'package:aves/widgets/album/collection_scaling.dart'; import 'package:aves/widgets/album/collection_section.dart'; import 'package:draggable_scrollbar/draggable_scrollbar.dart'; @@ -7,6 +7,8 @@ import 'package:provider/provider.dart'; class ThumbnailCollection extends StatelessWidget { final Widget appBar; + final WidgetBuilder emptyBuilder; + final ScrollController _scrollController = ScrollController(); final ValueNotifier _columnCountNotifier = ValueNotifier(4); final GlobalKey _scrollableKey = GlobalKey(); @@ -14,11 +16,12 @@ class ThumbnailCollection extends StatelessWidget { ThumbnailCollection({ Key key, this.appBar, + this.emptyBuilder, }) : super(key: key); @override Widget build(BuildContext context) { - final collection = Provider.of(context); + final collection = Provider.of(context); final sections = collection.sections; final sectionKeys = sections.keys.toList(); @@ -56,6 +59,12 @@ class ThumbnailCollection extends StatelessWidget { controller: _scrollController, slivers: [ if (appBar != null) appBar, + if (collection.isEmpty && emptyBuilder != null) + SliverFillViewport( + delegate: SliverChildListDelegate( + [emptyBuilder(context)], + ), + ), ...sectionKeys.map((sectionKey) => SectionSliver( collection: collection, sectionKey: sectionKey, diff --git a/lib/widgets/common/providers/media_store_collection_provider.dart b/lib/widgets/common/providers/media_store_collection_provider.dart index 0e8dfc86e..6aa2c6d1b 100644 --- a/lib/widgets/common/providers/media_store_collection_provider.dart +++ b/lib/widgets/common/providers/media_store_collection_provider.dart @@ -1,4 +1,5 @@ -import 'package:aves/model/image_collection.dart'; +import 'package:aves/model/collection_lens.dart'; +import 'package:aves/model/collection_source.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_file_service.dart'; import 'package:aves/model/metadata_db.dart'; @@ -18,7 +19,7 @@ class MediaStoreCollectionProvider extends StatefulWidget { } class _MediaStoreCollectionProviderState extends State { - Future collectionFuture; + Future collectionFuture; static const EventChannel eventChannel = EventChannel('deckers.thibault/aves/mediastore'); @@ -28,11 +29,14 @@ class _MediaStoreCollectionProviderState extends State _create() async { + Future _create() async { final stopwatch = Stopwatch()..start(); - final mediaStoreCollection = ImageCollection(entries: []); - mediaStoreCollection.groupFactor = settings.collectionGroupFactor; - mediaStoreCollection.sortFactor = settings.collectionSortFactor; + final mediaStoreSource = CollectionSource(); + final mediaStoreBaseLens = CollectionLens( + source: mediaStoreSource, + groupFactor: settings.collectionGroupFactor, + sortFactor: settings.collectionSortFactor, + ); await metadataDb.init(); // <20ms final currentTimeZone = await FlutterNativeTimezone.getLocalTimezone(); // <20ms @@ -44,17 +48,18 @@ class _MediaStoreCollectionProviderState extends State(); eventChannel.receiveBroadcastStream().cast().listen( - (entryMap) => mediaStoreCollection.add(ImageEntry.fromMap(entryMap)), + (entryMap) => allEntries.add(ImageEntry.fromMap(entryMap)), onDone: () async { debugPrint('$runtimeType stream complete in ${stopwatch.elapsed.inMilliseconds}ms'); - mediaStoreCollection.updateSections(); // <50ms + mediaStoreSource.addAll(allEntries); // TODO reduce setup time until here - mediaStoreCollection.updateAlbums(); // <50ms - await mediaStoreCollection.loadCatalogMetadata(); // 650ms - await mediaStoreCollection.catalogEntries(); // <50ms - await mediaStoreCollection.loadAddresses(); // 350ms - await mediaStoreCollection.locateEntries(); // <50ms + mediaStoreSource.updateAlbums(); // <50ms + await mediaStoreSource.loadCatalogMetadata(); // 650ms + await mediaStoreSource.catalogEntries(); // <50ms + await mediaStoreSource.loadAddresses(); // 350ms + await mediaStoreSource.locateEntries(); // <50ms debugPrint('$runtimeType setup end, elapsed=${stopwatch.elapsed}'); }, onError: (error) => debugPrint('$runtimeType mediastore stream error=$error'), @@ -63,16 +68,16 @@ class _MediaStoreCollectionProviderState extends State snapshot) { - final collection = (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) ? snapshot.data : ImageCollection(entries: []); - return ChangeNotifierProvider.value( + builder: (futureContext, AsyncSnapshot snapshot) { + final collection = (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) ? snapshot.data : CollectionLens.empty(); + return ChangeNotifierProvider.value( value: collection, child: widget.child, ); diff --git a/lib/widgets/fullscreen/fullscreen_action_delegate.dart b/lib/widgets/fullscreen/fullscreen_action_delegate.dart index 503fd7d17..d319ed46c 100644 --- a/lib/widgets/fullscreen/fullscreen_action_delegate.dart +++ b/lib/widgets/fullscreen/fullscreen_action_delegate.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:aves/model/image_collection.dart'; +import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/utils/android_app_service.dart'; import 'package:flushbar/flushbar.dart'; @@ -12,7 +12,7 @@ import 'package:printing/printing.dart'; enum FullscreenAction { delete, edit, info, open, openMap, print, rename, rotateCCW, rotateCW, setAs, share } class FullscreenActionDelegate { - final ImageCollection collection; + final CollectionLens collection; final VoidCallback showInfo; FullscreenActionDelegate({ @@ -109,7 +109,7 @@ class FullscreenActionDelegate { }, ); if (confirmed == null || !confirmed) return; - if (!await collection.delete(entry)) { + if (!await collection.source.delete(entry)) { _showFeedback(context, 'Failed'); } else if (collection.sortedEntries.isEmpty) { Navigator.pop(context); diff --git a/lib/widgets/fullscreen/fullscreen_page.dart b/lib/widgets/fullscreen/fullscreen_page.dart index bc326da19..6b322f744 100644 --- a/lib/widgets/fullscreen/fullscreen_page.dart +++ b/lib/widgets/fullscreen/fullscreen_page.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'dart:math'; -import 'package:aves/model/image_collection.dart'; +import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/fullscreen/fullscreen_action_delegate.dart'; @@ -19,7 +19,7 @@ import 'package:tuple/tuple.dart'; import 'package:video_player/video_player.dart'; class FullscreenPage extends AnimatedWidget { - final ImageCollection collection; + final CollectionLens collection; final String initialUri; const FullscreenPage({ @@ -44,7 +44,7 @@ class FullscreenPage extends AnimatedWidget { } class FullscreenBody extends StatefulWidget { - final ImageCollection collection; + final CollectionLens collection; final String initialUri; const FullscreenBody({ @@ -69,7 +69,7 @@ class FullscreenBodyState extends State with SingleTickerProvide FullscreenActionDelegate _actionDelegate; final List> _videoControllers = []; - ImageCollection get collection => widget.collection; + CollectionLens get collection => widget.collection; List get entries => widget.collection.sortedEntries; @@ -273,7 +273,7 @@ class FullscreenBodyState extends State with SingleTickerProvide } class FullscreenVerticalPageView extends StatefulWidget { - final ImageCollection collection; + final CollectionLens collection; final ImageEntry entry; final List> videoControllers; final PageController horizontalPager, verticalPager; diff --git a/lib/widgets/fullscreen/image_page.dart b/lib/widgets/fullscreen/image_page.dart index 013148111..8b9d0c311 100644 --- a/lib/widgets/fullscreen/image_page.dart +++ b/lib/widgets/fullscreen/image_page.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:aves/model/image_collection.dart'; +import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/widgets/fullscreen/video.dart'; import 'package:flutter/material.dart'; @@ -9,7 +9,7 @@ import 'package:tuple/tuple.dart'; import 'package:video_player/video_player.dart'; class ImagePage extends StatefulWidget { - final ImageCollection collection; + final CollectionLens collection; final PageController pageController; final VoidCallback onTap; final ValueChanged onPageChanged; diff --git a/lib/widgets/fullscreen/info/info_page.dart b/lib/widgets/fullscreen/info/info_page.dart index 34fa7dec8..4084cb77b 100644 --- a/lib/widgets/fullscreen/info/info_page.dart +++ b/lib/widgets/fullscreen/info/info_page.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/image_collection.dart'; +import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/widgets/common/coma_divider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; @@ -13,7 +13,7 @@ import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; class InfoPage extends StatefulWidget { - final ImageCollection collection; + final CollectionLens collection; final ImageEntry entry; final ValueNotifier visibleNotifier; diff --git a/lib/widgets/fullscreen/info/xmp_section.dart b/lib/widgets/fullscreen/info/xmp_section.dart index e1947c57b..72d992aad 100644 --- a/lib/widgets/fullscreen/info/xmp_section.dart +++ b/lib/widgets/fullscreen/info/xmp_section.dart @@ -1,4 +1,5 @@ -import 'package:aves/model/image_collection.dart'; +import 'package:aves/model/collection_filters.dart'; +import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/utils/color_utils.dart'; import 'package:aves/widgets/album/filtered_collection_page.dart'; @@ -6,7 +7,7 @@ import 'package:aves/widgets/fullscreen/info/info_page.dart'; import 'package:flutter/material.dart'; class XmpTagSectionSliver extends AnimatedWidget { - final ImageCollection collection; + final CollectionLens collection; final ImageEntry entry; static const double buttonBorderWidth = 2; @@ -56,7 +57,7 @@ class XmpTagSectionSliver extends AnimatedWidget { MaterialPageRoute( builder: (context) => FilteredCollectionPage( collection: collection, - filter: (entry) => entry.xmpSubjects.contains(tag), + filter: TagFilter(tag), title: tag, ), ), diff --git a/pubspec.lock b/pubspec.lock index ee2491e40..06d22eb98 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -80,6 +80,13 @@ packages: url: "git://github.com/deckerst/flutter-draggable-scrollbar.git" source: git version: "0.0.4" + event_bus: + dependency: "direct main" + description: + name: event_bus + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" flushbar: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 520038613..18e5669dd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: draggable_scrollbar: git: url: git://github.com/deckerst/flutter-draggable-scrollbar.git + event_bus: flushbar: flutter_native_timezone: flutter_staggered_grid_view: