190 lines
6.5 KiB
Dart
190 lines
6.5 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:aves/model/favourite_repo.dart';
|
|
import 'package:aves/model/filters/filters.dart';
|
|
import 'package:aves/model/image_entry.dart';
|
|
import 'package:aves/model/image_metadata.dart';
|
|
import 'package:aves/model/metadata_db.dart';
|
|
import 'package:aves/model/source/album.dart';
|
|
import 'package:aves/model/source/collection_lens.dart';
|
|
import 'package:aves/model/source/location.dart';
|
|
import 'package:aves/model/source/tag.dart';
|
|
import 'package:aves/services/image_file_service.dart';
|
|
import 'package:event_bus/event_bus.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import 'enums.dart';
|
|
|
|
mixin SourceBase {
|
|
final List<ImageEntry> _rawEntries = [];
|
|
|
|
List<ImageEntry> get rawEntries => List.unmodifiable(_rawEntries);
|
|
|
|
final EventBus _eventBus = EventBus();
|
|
|
|
EventBus get eventBus => _eventBus;
|
|
|
|
List<ImageEntry> get sortedEntriesForFilterList;
|
|
|
|
final Map<CollectionFilter, int> _filterEntryCountMap = {};
|
|
|
|
void invalidateFilterEntryCounts() => _filterEntryCountMap.clear();
|
|
|
|
final StreamController<ProgressEvent> _progressStreamController = StreamController.broadcast();
|
|
|
|
Stream<ProgressEvent> get progressStream => _progressStreamController.stream;
|
|
|
|
void setProgress({@required int done, @required int total}) => _progressStreamController.add(ProgressEvent(done: done, total: total));
|
|
}
|
|
|
|
class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin {
|
|
@override
|
|
List<ImageEntry> get sortedEntriesForFilterList => CollectionLens(
|
|
source: this,
|
|
groupFactor: EntryGroupFactor.none,
|
|
sortFactor: EntrySortFactor.date,
|
|
).sortedEntries;
|
|
|
|
ValueNotifier<SourceState> stateNotifier = ValueNotifier<SourceState>(SourceState.ready);
|
|
|
|
List<DateMetadata> _savedDates;
|
|
|
|
Future<void> loadDates() async {
|
|
final stopwatch = Stopwatch()..start();
|
|
_savedDates = List.unmodifiable(await metadataDb.loadDates());
|
|
debugPrint('$runtimeType loadDates complete in ${stopwatch.elapsed.inMilliseconds}ms for ${_savedDates.length} entries');
|
|
}
|
|
|
|
void addAll(Iterable<ImageEntry> entries) {
|
|
if (_rawEntries.isNotEmpty) {
|
|
final newContentIds = entries.map((entry) => entry.contentId).toList();
|
|
_rawEntries.removeWhere((entry) => newContentIds.contains(entry.contentId));
|
|
}
|
|
entries.forEach((entry) {
|
|
final contentId = entry.contentId;
|
|
entry.catalogDateMillis = _savedDates.firstWhere((metadata) => metadata.contentId == contentId, orElse: () => null)?.dateMillis;
|
|
});
|
|
_rawEntries.addAll(entries);
|
|
addFolderPath(_rawEntries.map((entry) => entry.directory));
|
|
invalidateFilterEntryCounts();
|
|
eventBus.fire(EntryAddedEvent());
|
|
}
|
|
|
|
void removeEntries(List<ImageEntry> entries) {
|
|
entries.forEach((entry) => entry.removeFromFavourites());
|
|
_rawEntries.removeWhere(entries.contains);
|
|
cleanEmptyAlbums(entries.map((entry) => entry.directory).toSet());
|
|
updateLocations();
|
|
updateTags();
|
|
invalidateFilterEntryCounts();
|
|
eventBus.fire(EntryRemovedEvent(entries));
|
|
}
|
|
|
|
void clearEntries() {
|
|
_rawEntries.clear();
|
|
cleanEmptyAlbums();
|
|
updateAlbums();
|
|
updateLocations();
|
|
updateTags();
|
|
invalidateFilterEntryCounts();
|
|
}
|
|
|
|
// `dateModifiedSecs` changes when moving entries to another directory,
|
|
// but it does not change when renaming the containing directory
|
|
Future<void> moveEntry(ImageEntry entry, Map newFields) async {
|
|
final oldContentId = entry.contentId;
|
|
final newContentId = newFields['contentId'] as int;
|
|
final newDateModifiedSecs = newFields['dateModifiedSecs'] as int;
|
|
if (newDateModifiedSecs != null) entry.dateModifiedSecs = newDateModifiedSecs;
|
|
entry.path = newFields['path'] as String;
|
|
entry.uri = newFields['uri'] as String;
|
|
entry.contentId = newContentId;
|
|
entry.catalogMetadata = entry.catalogMetadata?.copyWith(contentId: newContentId);
|
|
entry.addressDetails = entry.addressDetails?.copyWith(contentId: newContentId);
|
|
|
|
await metadataDb.updateEntryId(oldContentId, entry);
|
|
await metadataDb.updateMetadataId(oldContentId, entry.catalogMetadata);
|
|
await metadataDb.updateAddressId(oldContentId, entry.addressDetails);
|
|
await favourites.move(oldContentId, entry);
|
|
}
|
|
|
|
void updateAfterMove({
|
|
@required List<ImageEntry> selection,
|
|
@required bool copy,
|
|
@required String destinationAlbum,
|
|
@required Iterable<MoveOpEvent> movedOps,
|
|
}) async {
|
|
if (movedOps.isEmpty) return;
|
|
|
|
final fromAlbums = <String>{};
|
|
final movedEntries = <ImageEntry>[];
|
|
if (copy) {
|
|
movedOps.forEach((movedOp) {
|
|
final sourceUri = movedOp.uri;
|
|
final newFields = movedOp.newFields;
|
|
final sourceEntry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null);
|
|
fromAlbums.add(sourceEntry.directory);
|
|
movedEntries.add(sourceEntry?.copyWith(
|
|
uri: newFields['uri'] as String,
|
|
path: newFields['path'] as String,
|
|
contentId: newFields['contentId'] as int,
|
|
dateModifiedSecs: newFields['dateModifiedSecs'] as int,
|
|
));
|
|
});
|
|
await metadataDb.saveEntries(movedEntries);
|
|
await metadataDb.saveMetadata(movedEntries.map((entry) => entry.catalogMetadata));
|
|
await metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails));
|
|
} else {
|
|
await Future.forEach<MoveOpEvent>(movedOps, (movedOp) async {
|
|
final sourceUri = movedOp.uri;
|
|
final newFields = movedOp.newFields;
|
|
final entry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null);
|
|
if (entry != null) {
|
|
fromAlbums.add(entry.directory);
|
|
movedEntries.add(entry);
|
|
await moveEntry(entry, newFields);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (copy) {
|
|
addAll(movedEntries);
|
|
} else {
|
|
cleanEmptyAlbums(fromAlbums);
|
|
addFolderPath({destinationAlbum});
|
|
}
|
|
updateAlbums();
|
|
invalidateFilterEntryCounts();
|
|
eventBus.fire(EntryMovedEvent(movedEntries));
|
|
}
|
|
|
|
int count(CollectionFilter filter) {
|
|
return _filterEntryCountMap.putIfAbsent(filter, () => _rawEntries.where((entry) => filter.filter(entry)).length);
|
|
}
|
|
}
|
|
|
|
enum SourceState { loading, cataloguing, locating, ready }
|
|
|
|
class EntryAddedEvent {
|
|
final ImageEntry entry;
|
|
|
|
const EntryAddedEvent([this.entry]);
|
|
}
|
|
|
|
class EntryRemovedEvent {
|
|
final Iterable<ImageEntry> entries;
|
|
|
|
const EntryRemovedEvent(this.entries);
|
|
}
|
|
|
|
class EntryMovedEvent {
|
|
final Iterable<ImageEntry> entries;
|
|
|
|
const EntryMovedEvent(this.entries);
|
|
}
|
|
|
|
class ProgressEvent {
|
|
final int done, total;
|
|
|
|
const ProgressEvent({@required this.done, @required this.total});
|
|
}
|