reviewed collection model to work with source + lenses
This commit is contained in:
parent
382d509a31
commit
83f49902b9
20 changed files with 361 additions and 197 deletions
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
39
lib/model/collection_filters.dart
Normal file
39
lib/model/collection_filters.dart
Normal 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);
|
||||
}
|
157
lib/model/collection_lens.dart
Normal file
157
lib/model/collection_lens.dart
Normal 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 }
|
|
@ -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);
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 [
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue