reviewed collection model to work with source + lenses

This commit is contained in:
Thibault Deckers 2020-03-07 15:53:41 +09:00
parent 382d509a31
commit 83f49902b9
20 changed files with 361 additions and 197 deletions

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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<CollectionFilter> filters;
GroupFactor groupFactor;
SortFactor sortFactor;
List<ImageEntry> _filteredEntries;
List<StreamSubscription> _subscriptions = [];
Map<dynamic, List<ImageEntry>> sections = Map.unmodifiable({});
CollectionLens({
@required this.source,
List<CollectionFilter> filters,
GroupFactor groupFactor,
SortFactor sortFactor,
}) : this.filters = filters ?? [],
this.groupFactor = groupFactor ?? GroupFactor.month,
this.sortFactor = sortFactor ?? SortFactor.date {
_subscriptions.add(source.eventBus.on<EntryAddedEvent>().listen((e) => onSourceChanged()));
_subscriptions.add(source.eventBus.on<EntryRemovedEvent>().listen((e) => onSourceChanged()));
_subscriptions.add(source.eventBus.on<MetadataChangedEvent>().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<ImageEntry> 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<bool> 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 }

View file

@ -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<ImageEntry> _rawEntries;
Map<dynamic, List<ImageEntry>> sections = Map.unmodifiable({});
GroupFactor groupFactor = GroupFactor.month;
SortFactor sortFactor = SortFactor.date;
final EventBus _eventBus = EventBus();
List<String> sortedAlbums = List.unmodifiable(const Iterable.empty());
List<String> sortedTags = List.unmodifiable(const Iterable.empty());
ImageCollection({
@required List<ImageEntry> entries,
this.groupFactor,
this.sortFactor,
}) : _rawEntries = entries {
if (_rawEntries.isNotEmpty) updateSections();
}
List<ImageEntry> 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<ImageEntry> 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<bool> 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<ImageEntry> entries,
}) : _rawEntries = entries ?? [];
Future<void> 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<void> 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<String> 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<ImageEntry> entries) {
_rawEntries.addAll(entries);
eventBus.fire(const EntryAddedEvent());
}
Future<bool> 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<String> 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);
}

View file

@ -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';

View file

@ -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<ImageCollection>(context);
final tags = collection.sortedTags;
final collection = Provider.of<CollectionLens>(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,

View file

@ -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<Widget> _buildActions() {
return [
Builder(
builder: (context) => Consumer<ImageCollection>(
builder: (context) => Consumer<CollectionLens>(
builder: (context, collection, child) => IconButton(
icon: Icon(OMIcons.search),
onPressed: () => showSearch(
@ -42,7 +42,7 @@ class _AllCollectionAppBar extends SliverAppBar {
),
),
Builder(
builder: (context) => Consumer<ImageCollection>(
builder: (context) => Consumer<CollectionLens>(
builder: (context, collection, child) => PopupMenuButton<AlbumAction>(
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(

View file

@ -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<dynamic, List<ImageEntry>> sections;
final dynamic sectionKey;
@ -143,7 +144,7 @@ class SectionHeader extends StatelessWidget {
child: albumIcon,
);
}
var title = collection.getUniqueAlbumName(sectionKey as String, sections.keys.cast<String>());
final title = CollectionSource.getUniqueAlbumName(sectionKey as String, sections.keys.cast<String>());
return TitleSectionHeader(
key: ValueKey(title),
leading: albumIcon,

View file

@ -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<ImageCollection>.value(
body: ChangeNotifierProvider<CollectionLens>.value(
value: collection,
child: ThumbnailCollection(
appBar: SliverAppBar(

View file

@ -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<ImageEntry> {
final ImageCollection collection;
final CollectionLens collection;
ImageSearchDelegate(this.collection);
@ -54,19 +55,17 @@ class ImageSearchDelegate extends SearchDelegate<ImageEntry> {
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<ImageCollection>.value(
value: ImageCollection(
entries: matches,
child: ChangeNotifierProvider<CollectionLens>.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 [

View file

@ -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<int> _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<ImageCollection>(context);
final collection = Provider.of<CollectionLens>(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,

View file

@ -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<MediaStoreCollectionProvider> {
Future<ImageCollection> collectionFuture;
Future<CollectionLens> collectionFuture;
static const EventChannel eventChannel = EventChannel('deckers.thibault/aves/mediastore');
@ -28,11 +29,14 @@ class _MediaStoreCollectionProviderState extends State<MediaStoreCollectionProvi
collectionFuture = _create();
}
Future<ImageCollection> _create() async {
Future<CollectionLens> _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<MediaStoreCollectionProvi
settings.catalogTimeZone = currentTimeZone;
}
final allEntries = List<ImageEntry>();
eventChannel.receiveBroadcastStream().cast<Map>().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<MediaStoreCollectionProvi
// TODO split image fetch AND/OR cache fetch across sessions
await ImageFileService.getImageEntries(); // 460ms
return mediaStoreCollection;
return mediaStoreBaseLens;
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: collectionFuture,
builder: (futureContext, AsyncSnapshot<ImageCollection> snapshot) {
final collection = (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) ? snapshot.data : ImageCollection(entries: []);
return ChangeNotifierProvider<ImageCollection>.value(
builder: (futureContext, AsyncSnapshot<CollectionLens> snapshot) {
final collection = (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) ? snapshot.data : CollectionLens.empty();
return ChangeNotifierProvider<CollectionLens>.value(
value: collection,
child: widget.child,
);

View file

@ -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);

View file

@ -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<FullscreenBody> with SingleTickerProvide
FullscreenActionDelegate _actionDelegate;
final List<Tuple2<String, VideoPlayerController>> _videoControllers = [];
ImageCollection get collection => widget.collection;
CollectionLens get collection => widget.collection;
List<ImageEntry> get entries => widget.collection.sortedEntries;
@ -273,7 +273,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
}
class FullscreenVerticalPageView extends StatefulWidget {
final ImageCollection collection;
final CollectionLens collection;
final ImageEntry entry;
final List<Tuple2<String, VideoPlayerController>> videoControllers;
final PageController horizontalPager, verticalPager;

View file

@ -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<int> onPageChanged;

View file

@ -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<bool> visibleNotifier;

View file

@ -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,
),
),

View file

@ -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:

View file

@ -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: