#1107 dynamic albums
This commit is contained in:
parent
76f0764d27
commit
303425e699
97 changed files with 1439 additions and 753 deletions
|
@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
## <a id="unreleased"></a>[Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Albums: dynamic albums from filter sets
|
||||
|
||||
## <a id="v1.11.19"></a>[v1.11.19] - 2024-11-24
|
||||
|
||||
### Added
|
||||
|
|
|
@ -85,6 +85,7 @@
|
|||
"sourceStateLocatingPlaces": "Locating places",
|
||||
|
||||
"chipActionDelete": "Delete",
|
||||
"chipActionRemove": "Remove",
|
||||
"chipActionShowCollection": "Show in Collection",
|
||||
"chipActionGoToAlbumPage": "Show in Albums",
|
||||
"chipActionGoToCountryPage": "Show in Countries",
|
||||
|
@ -204,6 +205,7 @@
|
|||
"albumTierSpecial": "Common",
|
||||
"albumTierApps": "Apps",
|
||||
"albumTierVaults": "Vaults",
|
||||
"albumTierDynamic": "Dynamic",
|
||||
"albumTierRegular": "Others",
|
||||
|
||||
"coordinateFormatDms": "DMS",
|
||||
|
@ -427,6 +429,9 @@
|
|||
"newAlbumDialogNameLabelAlreadyExistsHelper": "Directory already exists",
|
||||
"newAlbumDialogStorageLabel": "Storage:",
|
||||
|
||||
"newDynamicAlbumDialogTitle": "New Dynamic Album",
|
||||
"dynamicAlbumAlreadyExists": "Dynamic album already exists",
|
||||
|
||||
"newVaultWarningDialogMessage": "Items in vaults are only available to this app and no others.\n\nIf you uninstall this app, or clear this app data, you will lose all these items.",
|
||||
"newVaultDialogTitle": "New Vault",
|
||||
"configureVaultDialogTitle": "Configure Vault",
|
||||
|
@ -595,6 +600,7 @@
|
|||
|
||||
"collectionActionShowTitleSearch": "Show title filter",
|
||||
"collectionActionHideTitleSearch": "Hide title filter",
|
||||
"collectionActionAddDynamicAlbum": "Add dynamic album",
|
||||
"collectionActionAddShortcut": "Add shortcut",
|
||||
"collectionActionSetHome": "Set as home",
|
||||
"collectionActionEmptyBin": "Empty bin",
|
||||
|
@ -806,6 +812,7 @@
|
|||
"settingsActionImportDialogTitle": "Import",
|
||||
|
||||
"appExportCovers": "Covers",
|
||||
"appExportDynamicAlbums": "Dynamic albums",
|
||||
"appExportFavourites": "Favorites",
|
||||
"appExportSettings": "Settings",
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'dart:async';
|
|||
|
||||
import 'package:aves/model/app_inventory.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/vaults/vaults.dart';
|
||||
|
@ -16,6 +16,8 @@ import 'package:flutter/painting.dart';
|
|||
|
||||
final Covers covers = Covers._private();
|
||||
|
||||
typedef CoverProps = (int? entryId, String? packageName, Color? color);
|
||||
|
||||
class Covers {
|
||||
final StreamController<Set<CollectionFilter>?> _entryChangeStreamController = StreamController.broadcast();
|
||||
final StreamController<Set<CollectionFilter>?> _packageChangeStreamController = StreamController.broadcast();
|
||||
|
@ -39,22 +41,60 @@ class Covers {
|
|||
|
||||
Set<CoverRow> get all => Set.unmodifiable(_rows);
|
||||
|
||||
(int? entryId, String? packageName, Color? color)? of(CollectionFilter filter) {
|
||||
if (filter is AlbumFilter && vaults.isLocked(filter.album)) return null;
|
||||
CoverProps? of(CollectionFilter filter) {
|
||||
if (filter is StoredAlbumFilter && vaults.isLocked(filter.album)) return null;
|
||||
|
||||
final row = _rows.firstWhereOrNull((row) => row.filter == filter);
|
||||
return row != null ? (row.entryId, row.packageName, row.color) : null;
|
||||
}
|
||||
|
||||
Future<CoverProps?> remove(CollectionFilter filter, {bool notify = true}) async {
|
||||
final props = of(filter);
|
||||
if (props != null) {
|
||||
await set(filter: filter, entryId: null, packageName: null, color: null);
|
||||
|
||||
if (notify) {
|
||||
final (entryId, packageName, color) = props;
|
||||
if (entryId != null) _entryChangeStreamController.add({filter});
|
||||
if (packageName != null) _packageChangeStreamController.add({filter});
|
||||
if (color != null) _colorChangeStreamController.add({filter});
|
||||
}
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
Future<void> removeAll(Set<CollectionFilter> filters, {bool notify = true}) async {
|
||||
final entryIdChanged = <CollectionFilter>{};
|
||||
final packageNameChanged = <CollectionFilter>{};
|
||||
final colorChanged = <CollectionFilter>{};
|
||||
|
||||
for (final filter in filters) {
|
||||
final props = await remove(filter, notify: false);
|
||||
if (notify && props != null) {
|
||||
final (entryId, packageName, color) = props;
|
||||
if (entryId != null) entryIdChanged.add(filter);
|
||||
if (packageName != null) packageNameChanged.add(filter);
|
||||
if (color != null) colorChanged.add(filter);
|
||||
}
|
||||
}
|
||||
|
||||
if (notify) {
|
||||
if (entryIdChanged.isNotEmpty) _entryChangeStreamController.add(entryIdChanged);
|
||||
if (packageNameChanged.isNotEmpty) _packageChangeStreamController.add(packageNameChanged);
|
||||
if (colorChanged.isNotEmpty) _colorChangeStreamController.add(colorChanged);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> set({
|
||||
required CollectionFilter filter,
|
||||
required int? entryId,
|
||||
required String? packageName,
|
||||
required Color? color,
|
||||
bool notify = true,
|
||||
}) async {
|
||||
// erase contextual properties from filters before saving them
|
||||
if (filter is AlbumFilter) {
|
||||
filter = AlbumFilter(filter.album, null);
|
||||
if (filter is StoredAlbumFilter) {
|
||||
filter = StoredAlbumFilter(filter.album, null);
|
||||
}
|
||||
|
||||
final oldRows = _rows.where((row) => row.filter == filter).toSet();
|
||||
|
@ -77,9 +117,11 @@ class Covers {
|
|||
await localMediaDb.addCovers({row});
|
||||
}
|
||||
|
||||
if (oldEntry != entryId) _entryChangeStreamController.add({filter});
|
||||
if (oldPackage != packageName) _packageChangeStreamController.add({filter});
|
||||
if (oldColor != color) _colorChangeStreamController.add({filter});
|
||||
if (notify) {
|
||||
if (oldEntry != entryId) _entryChangeStreamController.add({filter});
|
||||
if (oldPackage != packageName) _packageChangeStreamController.add({filter});
|
||||
if (oldColor != color) _colorChangeStreamController.add({filter});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _removeEntryFromRows(Set<CoverRow> rows) {
|
||||
|
@ -112,7 +154,7 @@ class Covers {
|
|||
}
|
||||
|
||||
AlbumType effectiveAlbumType(String albumPath) {
|
||||
final filterPackage = of(AlbumFilter(albumPath, null))?.$2;
|
||||
final filterPackage = of(StoredAlbumFilter(albumPath, null))?.$2;
|
||||
if (filterPackage != null) {
|
||||
return filterPackage.isEmpty ? AlbumType.regular : AlbumType.app;
|
||||
} else {
|
||||
|
@ -121,7 +163,7 @@ class Covers {
|
|||
}
|
||||
|
||||
String? effectiveAlbumPackage(String albumPath) {
|
||||
final filterPackage = of(AlbumFilter(albumPath, null))?.$2;
|
||||
final filterPackage = of(StoredAlbumFilter(albumPath, null))?.$2;
|
||||
return filterPackage ?? appInventory.getAlbumAppPackageName(albumPath);
|
||||
}
|
||||
|
||||
|
@ -129,7 +171,7 @@ class Covers {
|
|||
|
||||
List<Map<String, dynamic>>? export(CollectionSource source) {
|
||||
final visibleEntries = source.visibleEntries;
|
||||
final jsonList = covers.all
|
||||
final jsonList = all
|
||||
.map((row) {
|
||||
final entryId = row.entryId;
|
||||
final path = visibleEntries.firstWhereOrNull((entry) => entryId == entry.id)?.path;
|
||||
|
@ -180,7 +222,7 @@ class Covers {
|
|||
}
|
||||
|
||||
if (entry != null || packageName != null || colorValue != null) {
|
||||
covers.set(
|
||||
set(
|
||||
filter: filter,
|
||||
entryId: entry?.id,
|
||||
packageName: packageName,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/dynamic_albums.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/favourites.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
|
@ -109,6 +110,16 @@ abstract class LocalMediaDb {
|
|||
|
||||
Future<void> removeCovers(Set<CollectionFilter> filters);
|
||||
|
||||
// dynamic albums
|
||||
|
||||
Future<void> clearDynamicAlbums();
|
||||
|
||||
Future<Set<DynamicAlbumRow>> loadAllDynamicAlbums();
|
||||
|
||||
Future<void> addDynamicAlbums(Set<DynamicAlbumRow> rows);
|
||||
|
||||
Future<void> removeDynamicAlbums(Set<String> names);
|
||||
|
||||
// video playback
|
||||
|
||||
Future<void> clearVideoPlayback();
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:io';
|
|||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/db/db.dart';
|
||||
import 'package:aves/model/db/db_sqflite_upgrade.dart';
|
||||
import 'package:aves/model/dynamic_albums.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/favourites.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
|
@ -27,6 +28,7 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
|
|||
static const addressTable = 'address';
|
||||
static const favouriteTable = 'favourites';
|
||||
static const coverTable = 'covers';
|
||||
static const dynamicAlbumTable = 'dynamicAlbums';
|
||||
static const vaultTable = 'vaults';
|
||||
static const trashTable = 'trash';
|
||||
static const videoPlaybackTable = 'videoPlayback';
|
||||
|
@ -93,6 +95,10 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
|
|||
', packageName TEXT'
|
||||
', color INTEGER'
|
||||
')');
|
||||
await db.execute('CREATE TABLE $dynamicAlbumTable('
|
||||
'name TEXT PRIMARY KEY'
|
||||
', filter TEXT'
|
||||
')');
|
||||
await db.execute('CREATE TABLE $vaultTable('
|
||||
'name TEXT PRIMARY KEY'
|
||||
', autoLock INTEGER'
|
||||
|
@ -110,7 +116,7 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
|
|||
')');
|
||||
},
|
||||
onUpgrade: LocalMediaDbUpgrader.upgradeDb,
|
||||
version: 11,
|
||||
version: 12,
|
||||
);
|
||||
|
||||
final maxIdRows = await _db.rawQuery('SELECT MAX(id) AS maxId FROM $entryTable');
|
||||
|
@ -137,7 +143,7 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
|
|||
|
||||
final _dataTypes = dataTypes ?? EntryDataType.values.toSet();
|
||||
|
||||
// using array in `whereArgs` and using it with `where id IN ?` is a pain, so we prefer `batch` instead
|
||||
// using array in `whereArgs` and using it with `where arg IN ?` is a pain, so we prefer `batch` instead
|
||||
final batch = _db.batch();
|
||||
const where = 'id = ?';
|
||||
const coverWhere = 'entryId = ?';
|
||||
|
@ -450,7 +456,7 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
|
|||
Future<void> removeVaults(Set<VaultDetails> rows) async {
|
||||
if (rows.isEmpty) return;
|
||||
|
||||
// using array in `whereArgs` and using it with `where id IN ?` is a pain, so we prefer `batch` instead
|
||||
// using array in `whereArgs` and using it with `where arg IN ?` is a pain, so we prefer `batch` instead
|
||||
final batch = _db.batch();
|
||||
rows.map((v) => v.name).forEach((name) => batch.delete(vaultTable, where: 'name = ?', whereArgs: [name]));
|
||||
await batch.commit(noResult: true);
|
||||
|
@ -539,7 +545,7 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
|
|||
final ids = rows.map((row) => row.entryId);
|
||||
if (ids.isEmpty) return;
|
||||
|
||||
// using array in `whereArgs` and using it with `where id IN ?` is a pain, so we prefer `batch` instead
|
||||
// using array in `whereArgs` and using it with `where arg IN ?` is a pain, so we prefer `batch` instead
|
||||
final batch = _db.batch();
|
||||
ids.forEach((id) => batch.delete(favouriteTable, where: 'id = ?', whereArgs: [id]));
|
||||
await batch.commit(noResult: true);
|
||||
|
@ -609,12 +615,60 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
|
|||
}
|
||||
});
|
||||
|
||||
// using array in `whereArgs` and using it with `where filter IN ?` is a pain, so we prefer `batch` instead
|
||||
// using array in `whereArgs` and using it with `where arg IN ?` is a pain, so we prefer `batch` instead
|
||||
final batch = _db.batch();
|
||||
obsoleteFilterJson.forEach((filterJson) => batch.delete(coverTable, where: 'filter = ?', whereArgs: [filterJson]));
|
||||
await batch.commit(noResult: true);
|
||||
}
|
||||
|
||||
// dynamic albums
|
||||
|
||||
@override
|
||||
Future<void> clearDynamicAlbums() async {
|
||||
final count = await _db.delete(dynamicAlbumTable, where: '1');
|
||||
debugPrint('$runtimeType clearDynamicAlbums deleted $count rows');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Set<DynamicAlbumRow>> loadAllDynamicAlbums() async {
|
||||
final result = <DynamicAlbumRow>{};
|
||||
final cursor = await _db.queryCursor(dynamicAlbumTable, bufferSize: _queryCursorBufferSize);
|
||||
while (await cursor.moveNext()) {
|
||||
final row = DynamicAlbumRow.fromMap(cursor.current);
|
||||
if (row != null) {
|
||||
result.add(row);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addDynamicAlbums(Set<DynamicAlbumRow> rows) async {
|
||||
if (rows.isEmpty) return;
|
||||
|
||||
final batch = _db.batch();
|
||||
rows.forEach((row) => _batchInsertDynamicAlbum(batch, row));
|
||||
await batch.commit(noResult: true);
|
||||
}
|
||||
|
||||
void _batchInsertDynamicAlbum(Batch batch, DynamicAlbumRow row) {
|
||||
batch.insert(
|
||||
dynamicAlbumTable,
|
||||
row.toMap(),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> removeDynamicAlbums(Set<String> names) async {
|
||||
if (names.isEmpty) return;
|
||||
|
||||
// using array in `whereArgs` and using it with `where arg IN ?` is a pain, so we prefer `batch` instead
|
||||
final batch = _db.batch();
|
||||
names.forEach((name) => batch.delete(dynamicAlbumTable, where: 'name = ?', whereArgs: [name]));
|
||||
await batch.commit(noResult: true);
|
||||
}
|
||||
|
||||
// video playback
|
||||
|
||||
@override
|
||||
|
@ -665,7 +719,7 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
|
|||
Future<void> removeVideoPlayback(Set<int> ids) async {
|
||||
if (ids.isEmpty) return;
|
||||
|
||||
// using array in `whereArgs` and using it with `where filter IN ?` is a pain, so we prefer `batch` instead
|
||||
// using array in `whereArgs` and using it with `where arg IN ?` is a pain, so we prefer `batch` instead
|
||||
final batch = _db.batch();
|
||||
ids.forEach((id) => batch.delete(videoPlaybackTable, where: 'id = ?', whereArgs: [id]));
|
||||
await batch.commit(noResult: true);
|
||||
|
|
|
@ -9,6 +9,7 @@ class LocalMediaDbUpgrader {
|
|||
static const addressTable = SqfliteLocalMediaDb.addressTable;
|
||||
static const favouriteTable = SqfliteLocalMediaDb.favouriteTable;
|
||||
static const coverTable = SqfliteLocalMediaDb.coverTable;
|
||||
static const dynamicAlbumTable = SqfliteLocalMediaDb.dynamicAlbumTable;
|
||||
static const vaultTable = SqfliteLocalMediaDb.vaultTable;
|
||||
static const trashTable = SqfliteLocalMediaDb.trashTable;
|
||||
static const videoPlaybackTable = SqfliteLocalMediaDb.videoPlaybackTable;
|
||||
|
@ -38,6 +39,8 @@ class LocalMediaDbUpgrader {
|
|||
await _upgradeFrom9(db);
|
||||
case 10:
|
||||
await _upgradeFrom10(db);
|
||||
case 11:
|
||||
await _upgradeFrom11(db);
|
||||
}
|
||||
oldVersion++;
|
||||
}
|
||||
|
@ -376,4 +379,13 @@ class LocalMediaDbUpgrader {
|
|||
', lockType TEXT'
|
||||
')');
|
||||
}
|
||||
|
||||
static Future<void> _upgradeFrom11(Database db) async {
|
||||
debugPrint('upgrading DB from v11');
|
||||
|
||||
await db.execute('CREATE TABLE $dynamicAlbumTable('
|
||||
'name TEXT PRIMARY KEY'
|
||||
', filter TEXT'
|
||||
')');
|
||||
}
|
||||
}
|
||||
|
|
109
lib/model/dynamic_albums.dart
Normal file
109
lib/model/dynamic_albums.dart
Normal file
|
@ -0,0 +1,109 @@
|
|||
import 'package:aves/model/filters/covered/dynamic_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
final DynamicAlbums dynamicAlbums = DynamicAlbums._private();
|
||||
|
||||
class DynamicAlbums with ChangeNotifier {
|
||||
Set<DynamicAlbumFilter> _rows = {};
|
||||
|
||||
DynamicAlbums._private() {
|
||||
if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this);
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
_rows = (await localMediaDb.loadAllDynamicAlbums()).map((v) => DynamicAlbumFilter(v.name, v.filter)).toSet();
|
||||
}
|
||||
|
||||
int get count => _rows.length;
|
||||
|
||||
Set<DynamicAlbumFilter> get all => Set.unmodifiable(_rows);
|
||||
|
||||
Future<void> add(DynamicAlbumFilter filter) async {
|
||||
await localMediaDb.addDynamicAlbums({DynamicAlbumRow(name: filter.name, filter: filter.filter)});
|
||||
_rows.add(filter);
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> remove(Set<DynamicAlbumFilter> filters) async {
|
||||
await localMediaDb.removeDynamicAlbums(filters.map((filter) => filter.name).toSet());
|
||||
_rows.removeAll(filters);
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
await localMediaDb.clearDynamicAlbums();
|
||||
_rows.clear();
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> rename(DynamicAlbumFilter filter, String newName) async {
|
||||
await localMediaDb.removeDynamicAlbums({filter.name});
|
||||
_rows.remove(filter);
|
||||
|
||||
await add(DynamicAlbumFilter(newName, filter.filter));
|
||||
}
|
||||
|
||||
DynamicAlbumFilter? get(String name) => _rows.firstWhereOrNull((row) => row.name == name);
|
||||
|
||||
bool contains(String name) => get(name) != null;
|
||||
|
||||
// import/export
|
||||
|
||||
List<String>? export() {
|
||||
final jsonList = all.map((row) => row.toJson()).toList();
|
||||
return jsonList.isNotEmpty ? jsonList : null;
|
||||
}
|
||||
|
||||
void import(dynamic jsonList) {
|
||||
if (jsonList is! List) {
|
||||
debugPrint('failed to import dynamic albums for jsonMap=$jsonList');
|
||||
return;
|
||||
}
|
||||
|
||||
jsonList.forEach((row) {
|
||||
final filter = CollectionFilter.fromJson(row);
|
||||
if (filter == null || filter is! DynamicAlbumFilter) {
|
||||
debugPrint('failed to import dynamic album for row=$row');
|
||||
return;
|
||||
}
|
||||
|
||||
add(filter);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class DynamicAlbumRow extends Equatable {
|
||||
final String name;
|
||||
final CollectionFilter filter;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [name, filter];
|
||||
|
||||
const DynamicAlbumRow({
|
||||
required this.name,
|
||||
required this.filter,
|
||||
});
|
||||
|
||||
static DynamicAlbumRow? fromMap(Map map) {
|
||||
final filter = CollectionFilter.fromJson(map['filter']);
|
||||
if (filter == null) return null;
|
||||
|
||||
return DynamicAlbumRow(
|
||||
name: map['name'] as String,
|
||||
filter: filter,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'name': name,
|
||||
'filter': filter.toJson(),
|
||||
};
|
||||
}
|
|
@ -59,7 +59,7 @@ class Favourites with ChangeNotifier {
|
|||
|
||||
Map<String, List<String>>? export(CollectionSource source) {
|
||||
final visibleEntries = source.visibleEntries;
|
||||
final ids = favourites.all;
|
||||
final ids = all;
|
||||
final paths = visibleEntries.where((entry) => ids.contains(entry.id)).map((entry) => entry.path).nonNulls.toSet();
|
||||
final byVolume = groupBy<String, StorageVolume?>(paths, androidFileUtils.getStorageVolume);
|
||||
final jsonMap = Map.fromEntries(byVolume.entries.map((kv) {
|
||||
|
@ -97,7 +97,7 @@ class Favourites with ChangeNotifier {
|
|||
}
|
||||
|
||||
if (foundEntries.isNotEmpty) {
|
||||
favourites.add(foundEntries);
|
||||
add(foundEntries);
|
||||
}
|
||||
if (missedPaths.isNotEmpty) {
|
||||
debugPrint('failed to import favourites with ${missedPaths.length} missed paths');
|
||||
|
|
17
lib/model/filters/covered/covered.dart
Normal file
17
lib/model/filters/covered/covered.dart
Normal file
|
@ -0,0 +1,17 @@
|
|||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
@immutable
|
||||
mixin CoveredFilter on CollectionFilter {
|
||||
@override
|
||||
Future<Color> color(BuildContext context) {
|
||||
final customColor = covers.of(this)?.$3;
|
||||
if (customColor != null) {
|
||||
return SynchronousFuture(customColor);
|
||||
}
|
||||
return super.color(context);
|
||||
}
|
||||
}
|
||||
|
69
lib/model/filters/covered/dynamic_album.dart
Normal file
69
lib/model/filters/covered/dynamic_album.dart
Normal file
|
@ -0,0 +1,69 @@
|
|||
import 'package:aves/model/filters/covered/covered.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves_model/aves_model.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class DynamicAlbumFilter extends AlbumBaseFilter with CoveredFilter {
|
||||
static const type = 'dynamic_album';
|
||||
|
||||
final String name;
|
||||
final CollectionFilter filter;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [name, filter, reversed];
|
||||
|
||||
DynamicAlbumFilter(this.name, this.filter, {super.reversed = false});
|
||||
|
||||
static DynamicAlbumFilter? fromMap(Map<String, dynamic> json) {
|
||||
final filter = CollectionFilter.fromJson(json['filter']);
|
||||
if (filter == null) return null;
|
||||
|
||||
return DynamicAlbumFilter(
|
||||
json['name'],
|
||||
filter,
|
||||
reversed: json['reversed'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() => {
|
||||
'type': type,
|
||||
'name': name,
|
||||
'filter': filter.toJson(),
|
||||
'reversed': reversed,
|
||||
};
|
||||
|
||||
@override
|
||||
EntryFilter get positiveTest => filter.test;
|
||||
|
||||
@override
|
||||
bool get exclusiveProp => false;
|
||||
|
||||
@override
|
||||
String get universalLabel => name;
|
||||
|
||||
@override
|
||||
Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) {
|
||||
return allowGenericIcon ? Icon(AIcons.dynamicAlbum, size: size) : null;
|
||||
}
|
||||
|
||||
@override
|
||||
String get category => type;
|
||||
|
||||
@override
|
||||
String get key => '$type-$reversed-$name';
|
||||
|
||||
@override
|
||||
bool match(String query) => name.toUpperCase().contains(query);
|
||||
|
||||
@override
|
||||
StorageVolume? get storageVolume => null;
|
||||
|
||||
@override
|
||||
bool get canRename => true;
|
||||
|
||||
@override
|
||||
bool get isVault => false;
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/filters/covered/covered.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/emoji_utils.dart';
|
||||
|
@ -6,7 +7,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class LocationFilter extends CoveredCollectionFilter {
|
||||
class LocationFilter extends CollectionFilter with CoveredFilter {
|
||||
static const type = 'location';
|
||||
static const locationSeparator = ';';
|
||||
|
|
@ -1,15 +1,30 @@
|
|||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/filters/covered/covered.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/vaults/vaults.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/theme/colors.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_icons.dart';
|
||||
import 'package:aves_model/aves_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class AlbumFilter extends CoveredCollectionFilter {
|
||||
abstract class AlbumBaseFilter extends CollectionFilter {
|
||||
const AlbumBaseFilter({required super.reversed});
|
||||
|
||||
bool match(String query);
|
||||
|
||||
StorageVolume? get storageVolume;
|
||||
|
||||
bool get canRename;
|
||||
|
||||
bool get isVault;
|
||||
}
|
||||
|
||||
class StoredAlbumFilter extends AlbumBaseFilter with CoveredFilter {
|
||||
static const type = 'album';
|
||||
|
||||
final String album;
|
||||
|
@ -19,12 +34,12 @@ class AlbumFilter extends CoveredCollectionFilter {
|
|||
@override
|
||||
List<Object?> get props => [album, reversed];
|
||||
|
||||
AlbumFilter(this.album, this.displayName, {super.reversed = false}) {
|
||||
StoredAlbumFilter(this.album, this.displayName, {super.reversed = false}) {
|
||||
_test = (entry) => entry.directory == album;
|
||||
}
|
||||
|
||||
factory AlbumFilter.fromMap(Map<String, dynamic> json) {
|
||||
return AlbumFilter(
|
||||
factory StoredAlbumFilter.fromMap(Map<String, dynamic> json) {
|
||||
return StoredAlbumFilter(
|
||||
json['album'],
|
||||
json['uniqueName'],
|
||||
reversed: json['reversed'] ?? false,
|
||||
|
@ -95,7 +110,25 @@ class AlbumFilter extends CoveredCollectionFilter {
|
|||
@override
|
||||
String get category => type;
|
||||
|
||||
// key `album-{path}` is expected by test driver
|
||||
// key is expected by test driver
|
||||
@override
|
||||
String get key => '$type-$reversed-$album';
|
||||
|
||||
@override
|
||||
bool match(String query) => (displayName ?? album).toUpperCase().contains(query);
|
||||
|
||||
@override
|
||||
StorageVolume? get storageVolume => androidFileUtils.getStorageVolume(album);
|
||||
|
||||
@override
|
||||
bool get canRename {
|
||||
if (isVault) return true;
|
||||
|
||||
// do not allow renaming volume root
|
||||
final dir = androidFileUtils.relativeDirectoryFromPath(album);
|
||||
return dir != null && dir.relativeDir.isNotEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isVault => vaults.isVault(album);
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
import 'package:aves/model/filters/covered/covered.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class TagFilter extends CoveredCollectionFilter {
|
||||
class TagFilter extends CollectionFilter with CoveredFilter {
|
||||
static const type = 'tag';
|
||||
|
||||
final String tag;
|
|
@ -1,22 +1,23 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/aspect_ratio.dart';
|
||||
import 'package:aves/model/filters/coordinate.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/covered/dynamic_album.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/filters/covered/tag.dart';
|
||||
import 'package:aves/model/filters/date.dart';
|
||||
import 'package:aves/model/filters/favourite.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/mime.dart';
|
||||
import 'package:aves/model/filters/missing.dart';
|
||||
import 'package:aves/model/filters/or.dart';
|
||||
import 'package:aves/model/filters/path.dart';
|
||||
import 'package:aves/model/filters/placeholder.dart';
|
||||
import 'package:aves/model/filters/query.dart';
|
||||
import 'package:aves/model/filters/rating.dart';
|
||||
import 'package:aves/model/filters/recent.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/filters/set_and.dart';
|
||||
import 'package:aves/model/filters/set_or.dart';
|
||||
import 'package:aves/model/filters/trash.dart';
|
||||
import 'package:aves/model/filters/type.dart';
|
||||
import 'package:aves/theme/colors.dart';
|
||||
|
@ -31,8 +32,11 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
|||
static const List<String> categoryOrder = [
|
||||
TrashFilter.type,
|
||||
QueryFilter.type,
|
||||
SetAndFilter.type,
|
||||
SetOrFilter.type,
|
||||
MimeFilter.type,
|
||||
AlbumFilter.type,
|
||||
DynamicAlbumFilter.type,
|
||||
StoredAlbumFilter.type,
|
||||
TypeFilter.type,
|
||||
RecentlyAddedFilter.type,
|
||||
DateFilter.type,
|
||||
|
@ -44,7 +48,6 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
|||
AspectRatioFilter.type,
|
||||
MissingFilter.type,
|
||||
PathFilter.type,
|
||||
OrFilter.type,
|
||||
];
|
||||
|
||||
final bool reversed;
|
||||
|
@ -54,14 +57,14 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
|||
static CollectionFilter? _fromMap(Map<String, dynamic> jsonMap) {
|
||||
final type = jsonMap['type'];
|
||||
switch (type) {
|
||||
case AlbumFilter.type:
|
||||
return AlbumFilter.fromMap(jsonMap);
|
||||
case AspectRatioFilter.type:
|
||||
return AspectRatioFilter.fromMap(jsonMap);
|
||||
case CoordinateFilter.type:
|
||||
return CoordinateFilter.fromMap(jsonMap);
|
||||
case DateFilter.type:
|
||||
return DateFilter.fromMap(jsonMap);
|
||||
case DynamicAlbumFilter.type:
|
||||
return DynamicAlbumFilter.fromMap(jsonMap);
|
||||
case FavouriteFilter.type:
|
||||
return FavouriteFilter.fromMap(jsonMap);
|
||||
case LocationFilter.type:
|
||||
|
@ -70,8 +73,10 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
|||
return MimeFilter.fromMap(jsonMap);
|
||||
case MissingFilter.type:
|
||||
return MissingFilter.fromMap(jsonMap);
|
||||
case OrFilter.type:
|
||||
return OrFilter.fromMap(jsonMap);
|
||||
case SetAndFilter.type:
|
||||
return SetAndFilter.fromMap(jsonMap);
|
||||
case SetOrFilter.type:
|
||||
return SetOrFilter.fromMap(jsonMap);
|
||||
case PathFilter.type:
|
||||
return PathFilter.fromMap(jsonMap);
|
||||
case PlaceholderFilter.type:
|
||||
|
@ -82,6 +87,8 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
|||
return RatingFilter.fromMap(jsonMap);
|
||||
case RecentlyAddedFilter.type:
|
||||
return RecentlyAddedFilter.fromMap(jsonMap);
|
||||
case StoredAlbumFilter.type:
|
||||
return StoredAlbumFilter.fromMap(jsonMap);
|
||||
case TagFilter.type:
|
||||
return TagFilter.fromMap(jsonMap);
|
||||
case TypeFilter.type:
|
||||
|
@ -155,20 +162,6 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
|||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
abstract class CoveredCollectionFilter extends CollectionFilter {
|
||||
const CoveredCollectionFilter({required super.reversed});
|
||||
|
||||
@override
|
||||
Future<Color> color(BuildContext context) {
|
||||
final customColor = covers.of(this)?.$3;
|
||||
if (customColor != null) {
|
||||
return SynchronousFuture(customColor);
|
||||
}
|
||||
return super.color(context);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class FilterGridItem<T extends CollectionFilter> with EquatableMixin {
|
||||
final T filter;
|
||||
|
|
75
lib/model/filters/set_and.dart
Normal file
75
lib/model/filters/set_and.dart
Normal file
|
@ -0,0 +1,75 @@
|
|||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class SetAndFilter extends CollectionFilter {
|
||||
static const type = 'and';
|
||||
|
||||
late final List<CollectionFilter> _filters;
|
||||
|
||||
late final EntryFilter _test;
|
||||
late final IconData? _genericIcon;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [_filters, reversed];
|
||||
|
||||
CollectionFilter get _first => _filters.first;
|
||||
|
||||
SetAndFilter(Set<CollectionFilter> filters, {super.reversed = false}) {
|
||||
_filters = filters.toList().sorted();
|
||||
_test = (entry) => _filters.every((v) => v.test(entry));
|
||||
switch (_first) {
|
||||
case StoredAlbumFilter():
|
||||
_genericIcon = AIcons.album;
|
||||
case LocationFilter(level: LocationLevel.country):
|
||||
_genericIcon = AIcons.country;
|
||||
case LocationFilter(level: LocationLevel.state):
|
||||
_genericIcon = AIcons.state;
|
||||
default:
|
||||
_genericIcon = null;
|
||||
}
|
||||
}
|
||||
|
||||
static SetAndFilter? fromMap(Map<String, dynamic> json) {
|
||||
final filters = (json['filters'] as List).cast<String>().map(CollectionFilter.fromJson).nonNulls.toSet();
|
||||
if (filters.isEmpty) return null;
|
||||
|
||||
return SetAndFilter(
|
||||
filters,
|
||||
reversed: json['reversed'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() => {
|
||||
'type': type,
|
||||
'filters': _filters.map((v) => v.toJson()).toList(),
|
||||
'reversed': reversed,
|
||||
};
|
||||
|
||||
@override
|
||||
EntryFilter get positiveTest => _test;
|
||||
|
||||
@override
|
||||
bool get exclusiveProp => false;
|
||||
|
||||
@override
|
||||
String get universalLabel => _filters.map((v) => v.universalLabel).join(', ');
|
||||
|
||||
@override
|
||||
String getLabel(BuildContext context) => _filters.map((v) => v.getLabel(context)).join(', ');
|
||||
|
||||
@override
|
||||
Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) {
|
||||
return _genericIcon != null ? Icon(_genericIcon, size: size) : _first.iconBuilder(context, size, allowGenericIcon: allowGenericIcon);
|
||||
}
|
||||
|
||||
@override
|
||||
String get category => _first.category;
|
||||
|
||||
@override
|
||||
String get key => '$type-$reversed-${_filters.map((v) => v.key)}';
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class OrFilter extends CollectionFilter {
|
||||
class SetOrFilter extends CollectionFilter {
|
||||
static const type = 'or';
|
||||
|
||||
late final List<CollectionFilter> _filters;
|
||||
|
@ -18,11 +18,11 @@ class OrFilter extends CollectionFilter {
|
|||
|
||||
CollectionFilter get _first => _filters.first;
|
||||
|
||||
OrFilter(Set<CollectionFilter> filters, {super.reversed = false}) {
|
||||
SetOrFilter(Set<CollectionFilter> filters, {super.reversed = false}) {
|
||||
_filters = filters.toList().sorted();
|
||||
_test = (entry) => _filters.any((v) => v.test(entry));
|
||||
switch (_first) {
|
||||
case AlbumFilter():
|
||||
case StoredAlbumFilter():
|
||||
_genericIcon = AIcons.album;
|
||||
case LocationFilter(level: LocationLevel.country):
|
||||
_genericIcon = AIcons.country;
|
||||
|
@ -33,9 +33,12 @@ class OrFilter extends CollectionFilter {
|
|||
}
|
||||
}
|
||||
|
||||
factory OrFilter.fromMap(Map<String, dynamic> json) {
|
||||
return OrFilter(
|
||||
(json['filters'] as List).cast<String>().map(CollectionFilter.fromJson).nonNulls.toSet(),
|
||||
static SetOrFilter? fromMap(Map<String, dynamic> json) {
|
||||
final filters = (json['filters'] as List).cast<String>().map(CollectionFilter.fromJson).nonNulls.toSet();
|
||||
if (filters.isEmpty) return null;
|
||||
|
||||
return SetOrFilter(
|
||||
filters,
|
||||
reversed: json['reversed'] ?? false,
|
||||
);
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/settings/defaults.dart';
|
||||
import 'package:aves_model/aves_model.dart';
|
||||
|
@ -64,9 +65,9 @@ mixin NavigationSettings on SettingsAccess {
|
|||
|
||||
set drawerTypeBookmarks(List<CollectionFilter?> newValue) => set(SettingKeys.drawerTypeBookmarksKey, newValue.map((filter) => filter?.toJson() ?? '').toList());
|
||||
|
||||
List<String>? get drawerAlbumBookmarks => getStringList(SettingKeys.drawerAlbumBookmarksKey);
|
||||
List<AlbumBaseFilter>? get drawerAlbumBookmarks => getStringList(SettingKeys.drawerAlbumBookmarksKey)?.map(CollectionFilter.fromJson).whereType<AlbumBaseFilter>().toList();
|
||||
|
||||
set drawerAlbumBookmarks(List<String>? newValue) => set(SettingKeys.drawerAlbumBookmarksKey, newValue);
|
||||
set drawerAlbumBookmarks(List<AlbumBaseFilter>? newValue) => set(SettingKeys.drawerAlbumBookmarksKey, newValue?.map((filter) => filter.toJson()).toList());
|
||||
|
||||
List<String> get drawerPageBookmarks => getStringList(SettingKeys.drawerPageBookmarksKey) ?? SettingsDefaults.drawerPageBookmarks;
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/dynamic_album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/vaults/vaults.dart';
|
||||
|
@ -17,13 +18,13 @@ mixin AlbumMixin on SourceBase {
|
|||
|
||||
List<String> get rawAlbums => List.unmodifiable(_directories);
|
||||
|
||||
Set<AlbumFilter> getNewAlbumFilters(BuildContext context) => Set.unmodifiable(_newAlbums.map((v) => AlbumFilter(v, getAlbumDisplayName(context, v))));
|
||||
Set<StoredAlbumFilter> getNewAlbumFilters(BuildContext context) => Set.unmodifiable(_newAlbums.map((v) => StoredAlbumFilter(v, getStoredAlbumDisplayName(context, v))));
|
||||
|
||||
int compareAlbumsByName(String? a, String? b) {
|
||||
a ??= '';
|
||||
b ??= '';
|
||||
final ua = getAlbumDisplayName(null, a);
|
||||
final ub = getAlbumDisplayName(null, b);
|
||||
final ua = getStoredAlbumDisplayName(null, a);
|
||||
final ub = getStoredAlbumDisplayName(null, b);
|
||||
final c = compareAsciiUpperCaseNatural(ua, ub);
|
||||
if (c != 0) return c;
|
||||
final va = androidFileUtils.getStorageVolume(a)?.path ?? '';
|
||||
|
@ -36,7 +37,7 @@ mixin AlbumMixin on SourceBase {
|
|||
}
|
||||
|
||||
void _onAlbumChanged({bool notify = true}) {
|
||||
invalidateAlbumDisplayNames();
|
||||
invalidateStoredAlbumDisplayNames();
|
||||
if (notify) {
|
||||
notifyAlbumsChanged();
|
||||
}
|
||||
|
@ -82,12 +83,6 @@ mixin AlbumMixin on SourceBase {
|
|||
_directories.removeAll(removableAlbums);
|
||||
_onAlbumChanged();
|
||||
invalidateAlbumFilterSummary(directories: removableAlbums);
|
||||
|
||||
final bookmarks = settings.drawerAlbumBookmarks;
|
||||
removableAlbums.forEach((album) {
|
||||
bookmarks?.remove(album);
|
||||
});
|
||||
settings.drawerAlbumBookmarks = bookmarks;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,13 +90,13 @@ mixin AlbumMixin on SourceBase {
|
|||
if (visibleEntries.any((entry) => entry.directory == album)) return false;
|
||||
if (_newAlbums.contains(album)) return false;
|
||||
if (vaults.isVault(album)) return false;
|
||||
if (settings.pinnedFilters.whereType<AlbumFilter>().map((v) => v.album).contains(album)) return false;
|
||||
if (settings.pinnedFilters.whereType<StoredAlbumFilter>().map((v) => v.album).contains(album)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// filter summary
|
||||
|
||||
// by directory
|
||||
// by filter key
|
||||
final Map<String, int> _filterEntryCountMap = {}, _filterSizeMap = {};
|
||||
final Map<String, AvesEntry?> _filterRecentEntryMap = {};
|
||||
|
||||
|
@ -117,36 +112,53 @@ mixin AlbumMixin on SourceBase {
|
|||
_filterSizeMap.clear();
|
||||
_filterRecentEntryMap.clear();
|
||||
} else {
|
||||
// clear entries only for modified album directories
|
||||
directories ??= {};
|
||||
if (entries != null) {
|
||||
directories.addAll(entries.map((entry) => entry.directory).nonNulls);
|
||||
}
|
||||
directories.forEach((directory) {
|
||||
_filterEntryCountMap.remove(directory);
|
||||
_filterSizeMap.remove(directory);
|
||||
_filterRecentEntryMap.remove(directory);
|
||||
directories.nonNulls.map((v) => StoredAlbumFilter(v, null).key).forEach((key) {
|
||||
_filterEntryCountMap.remove(key);
|
||||
_filterSizeMap.remove(key);
|
||||
_filterRecentEntryMap.remove(key);
|
||||
});
|
||||
|
||||
// clear entries for all dynamic albums
|
||||
invalidateDynamicAlbumFilterSummary(notify: false);
|
||||
}
|
||||
if (notify) {
|
||||
eventBus.fire(AlbumSummaryInvalidatedEvent(directories));
|
||||
eventBus.fire(StoredAlbumSummaryInvalidatedEvent(directories));
|
||||
eventBus.fire(const DynamicAlbumSummaryInvalidatedEvent());
|
||||
}
|
||||
}
|
||||
|
||||
int albumEntryCount(AlbumFilter filter) {
|
||||
return _filterEntryCountMap.putIfAbsent(filter.album, () => visibleEntries.where(filter.test).length);
|
||||
void invalidateDynamicAlbumFilterSummary({bool notify = true}) {
|
||||
_filterEntryCountMap.removeWhere(_isDynamicAlbumKey);
|
||||
_filterSizeMap.removeWhere(_isDynamicAlbumKey);
|
||||
_filterRecentEntryMap.removeWhere(_isDynamicAlbumKey);
|
||||
|
||||
if (notify) {
|
||||
eventBus.fire(const DynamicAlbumSummaryInvalidatedEvent());
|
||||
}
|
||||
}
|
||||
|
||||
int albumSize(AlbumFilter filter) {
|
||||
return _filterSizeMap.putIfAbsent(filter.album, () => visibleEntries.where(filter.test).map((v) => v.sizeBytes).sum);
|
||||
bool _isDynamicAlbumKey(String key, _) => key.startsWith('${DynamicAlbumFilter.type}-');
|
||||
|
||||
int albumEntryCount(AlbumBaseFilter filter) {
|
||||
return _filterEntryCountMap.putIfAbsent(filter.key, () => visibleEntries.where(filter.test).length);
|
||||
}
|
||||
|
||||
AvesEntry? albumRecentEntry(AlbumFilter filter) {
|
||||
return _filterRecentEntryMap.putIfAbsent(filter.album, () => sortedEntriesByDate.firstWhereOrNull(filter.test));
|
||||
int albumSize(AlbumBaseFilter filter) {
|
||||
return _filterSizeMap.putIfAbsent(filter.key, () => visibleEntries.where(filter.test).map((v) => v.sizeBytes).sum);
|
||||
}
|
||||
|
||||
AvesEntry? albumRecentEntry(AlbumBaseFilter filter) {
|
||||
return _filterRecentEntryMap.putIfAbsent(filter.key, () => sortedEntriesByDate.firstWhereOrNull(filter.test));
|
||||
}
|
||||
|
||||
// new albums
|
||||
|
||||
void createAlbum(String directory) {
|
||||
void createStoredAlbum(String directory) {
|
||||
if (!_directories.contains(directory)) {
|
||||
_newAlbums.add(directory);
|
||||
addDirectories(albums: {directory});
|
||||
|
@ -156,7 +168,7 @@ mixin AlbumMixin on SourceBase {
|
|||
void renameNewAlbum(String source, String destination) {
|
||||
if (_newAlbums.remove(source)) {
|
||||
cleanEmptyAlbums({source});
|
||||
createAlbum(destination);
|
||||
createStoredAlbum(destination);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -168,12 +180,12 @@ mixin AlbumMixin on SourceBase {
|
|||
|
||||
final Map<String, String> _albumDisplayNamesWithContext = {}, _albumDisplayNamesWithoutContext = {};
|
||||
|
||||
void invalidateAlbumDisplayNames() {
|
||||
void invalidateStoredAlbumDisplayNames() {
|
||||
_albumDisplayNamesWithContext.clear();
|
||||
_albumDisplayNamesWithoutContext.clear();
|
||||
}
|
||||
|
||||
String _computeDisplayName(BuildContext? context, String dirPath) {
|
||||
String _computeStoredAlbumDisplayName(BuildContext? context, String dirPath) {
|
||||
final separator = pContext.separator;
|
||||
assert(!dirPath.endsWith(separator));
|
||||
|
||||
|
@ -222,16 +234,20 @@ mixin AlbumMixin on SourceBase {
|
|||
}
|
||||
}
|
||||
|
||||
String getAlbumDisplayName(BuildContext? context, String dirPath) {
|
||||
String getStoredAlbumDisplayName(BuildContext? context, String dirPath) {
|
||||
final names = (context != null ? _albumDisplayNamesWithContext : _albumDisplayNamesWithoutContext);
|
||||
return names.putIfAbsent(dirPath, () => _computeDisplayName(context, dirPath));
|
||||
return names.putIfAbsent(dirPath, () => _computeStoredAlbumDisplayName(context, dirPath));
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumsChangedEvent {}
|
||||
|
||||
class AlbumSummaryInvalidatedEvent {
|
||||
class DynamicAlbumSummaryInvalidatedEvent {
|
||||
const DynamicAlbumSummaryInvalidatedEvent();
|
||||
}
|
||||
|
||||
class StoredAlbumSummaryInvalidatedEvent {
|
||||
final Set<String?>? directories;
|
||||
|
||||
const AlbumSummaryInvalidatedEvent(this.directories);
|
||||
const StoredAlbumSummaryInvalidatedEvent(this.directories);
|
||||
}
|
||||
|
|
|
@ -6,10 +6,10 @@ import 'package:aves/model/entry/extensions/multipage.dart';
|
|||
import 'package:aves/model/entry/extensions/props.dart';
|
||||
import 'package:aves/model/entry/sort.dart';
|
||||
import 'package:aves/model/favourites.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/favourite.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/filters/mime.dart';
|
||||
import 'package:aves/model/filters/query.dart';
|
||||
import 'package:aves/model/filters/rating.dart';
|
||||
|
@ -143,7 +143,7 @@ class CollectionLens with ChangeNotifier {
|
|||
}
|
||||
|
||||
bool get showHeaders {
|
||||
bool showAlbumHeaders() => !filters.any((v) => v is AlbumFilter && !v.reversed);
|
||||
bool showAlbumHeaders() => !filters.any((v) => v is StoredAlbumFilter && !v.reversed);
|
||||
|
||||
switch (sortFactor) {
|
||||
case EntrySortFactor.date:
|
||||
|
|
|
@ -7,10 +7,10 @@ import 'package:aves/model/entry/extensions/catalog.dart';
|
|||
import 'package:aves/model/entry/extensions/location.dart';
|
||||
import 'package:aves/model/entry/sort.dart';
|
||||
import 'package:aves/model/favourites.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/covered/tag.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/filters/trash.dart';
|
||||
import 'package:aves/model/metadata/trash.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
|
@ -75,7 +75,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
|||
object: this,
|
||||
);
|
||||
}
|
||||
settings.updateStream.where((event) => event.key == SettingKeys.localeKey).listen((_) => invalidateAlbumDisplayNames());
|
||||
settings.updateStream.where((event) => event.key == SettingKeys.localeKey).listen((_) => invalidateStoredAlbumDisplayNames());
|
||||
settings.updateStream.where((event) => event.key == SettingKeys.hiddenFiltersKey).listen((event) {
|
||||
final oldValue = event.oldValue;
|
||||
if (oldValue is List<String>?) {
|
||||
|
@ -144,7 +144,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
|||
|
||||
Set<CollectionFilter> _getAppHiddenFilters() => {
|
||||
...settings.hiddenFilters,
|
||||
...vaults.vaultDirectories.where(vaults.isLocked).map((v) => AlbumFilter(v, null)),
|
||||
...vaults.vaultDirectories.where(vaults.isLocked).map((v) => StoredAlbumFilter(v, null)),
|
||||
};
|
||||
|
||||
Iterable<AvesEntry> _applyHiddenFilters(Iterable<AvesEntry> entries) {
|
||||
|
@ -288,11 +288,10 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> renameAlbum(String sourceAlbum, String destinationAlbum, Set<AvesEntry> entries, Set<MoveOpEvent> movedOps) async {
|
||||
final oldFilter = AlbumFilter(sourceAlbum, null);
|
||||
final newFilter = AlbumFilter(destinationAlbum, null);
|
||||
Future<void> renameStoredAlbum(String sourceAlbum, String destinationAlbum, Set<AvesEntry> entries, Set<MoveOpEvent> movedOps) async {
|
||||
final oldFilter = StoredAlbumFilter(sourceAlbum, null);
|
||||
final newFilter = StoredAlbumFilter(destinationAlbum, null);
|
||||
|
||||
final bookmark = settings.drawerAlbumBookmarks?.indexOf(sourceAlbum);
|
||||
final pinned = settings.pinnedFilters.contains(oldFilter);
|
||||
|
||||
if (vaults.isVault(sourceAlbum)) {
|
||||
|
@ -315,10 +314,17 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
|||
movedOps: movedOps,
|
||||
);
|
||||
|
||||
// restore bookmark and pin, as the obsolete album got removed and its associated state cleaned
|
||||
if (bookmark != null && bookmark != -1) {
|
||||
settings.drawerAlbumBookmarks = settings.drawerAlbumBookmarks?..insert(bookmark, destinationAlbum);
|
||||
// update bookmark
|
||||
final albumBookmarks = settings.drawerAlbumBookmarks;
|
||||
if (albumBookmarks != null) {
|
||||
final index = albumBookmarks.indexWhere((v) => v is StoredAlbumFilter && v.album == sourceAlbum);
|
||||
if (index >= 0) {
|
||||
albumBookmarks.removeAt(index);
|
||||
albumBookmarks.insert(index, newFilter);
|
||||
settings.drawerAlbumBookmarks = albumBookmarks;
|
||||
}
|
||||
}
|
||||
// restore pin, as the obsolete album got removed and its associated state cleaned
|
||||
if (pinned) {
|
||||
settings.pinnedFilters = settings.pinnedFilters
|
||||
..remove(oldFilter)
|
||||
|
@ -541,8 +547,9 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
|||
// filter summary
|
||||
|
||||
int count(CollectionFilter filter) {
|
||||
if (filter is AlbumFilter) return albumEntryCount(filter);
|
||||
if (filter is LocationFilter) {
|
||||
if (filter is AlbumBaseFilter) {
|
||||
return albumEntryCount(filter);
|
||||
} else if (filter is LocationFilter) {
|
||||
switch (filter.level) {
|
||||
case LocationLevel.country:
|
||||
return countryEntryCount(filter);
|
||||
|
@ -551,14 +558,16 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
|||
case LocationLevel.place:
|
||||
return placeEntryCount(filter);
|
||||
}
|
||||
} else if (filter is TagFilter) {
|
||||
return tagEntryCount(filter);
|
||||
}
|
||||
if (filter is TagFilter) return tagEntryCount(filter);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int size(CollectionFilter filter) {
|
||||
if (filter is AlbumFilter) return albumSize(filter);
|
||||
if (filter is LocationFilter) {
|
||||
if (filter is AlbumBaseFilter) {
|
||||
return albumSize(filter);
|
||||
} else if (filter is LocationFilter) {
|
||||
switch (filter.level) {
|
||||
case LocationLevel.country:
|
||||
return countrySize(filter);
|
||||
|
@ -567,14 +576,16 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
|||
case LocationLevel.place:
|
||||
return placeSize(filter);
|
||||
}
|
||||
} else if (filter is TagFilter) {
|
||||
return tagSize(filter);
|
||||
}
|
||||
if (filter is TagFilter) return tagSize(filter);
|
||||
return 0;
|
||||
}
|
||||
|
||||
AvesEntry? recentEntry(CollectionFilter filter) {
|
||||
if (filter is AlbumFilter) return albumRecentEntry(filter);
|
||||
if (filter is LocationFilter) {
|
||||
if (filter is AlbumBaseFilter) {
|
||||
return albumRecentEntry(filter);
|
||||
} else if (filter is LocationFilter) {
|
||||
switch (filter.level) {
|
||||
case LocationLevel.country:
|
||||
return countryRecentEntry(filter);
|
||||
|
@ -583,8 +594,9 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
|||
case LocationLevel.place:
|
||||
return placeRecentEntry(filter);
|
||||
}
|
||||
} else if (filter is TagFilter) {
|
||||
return tagRecentEntry(filter);
|
||||
}
|
||||
if (filter is TagFilter) return tagRecentEntry(filter);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -608,7 +620,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
|||
}
|
||||
|
||||
void _onVaultsChanged() {
|
||||
final newlyVisibleFilters = vaults.vaultDirectories.whereNot(vaults.isLocked).map((v) => AlbumFilter(v, null)).toSet();
|
||||
final newlyVisibleFilters = vaults.vaultDirectories.whereNot(vaults.isLocked).map((v) => StoredAlbumFilter(v, null)).toSet();
|
||||
_onFilterVisibilityChanged(newlyVisibleFilters);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/utils/collection_utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
|
|
@ -3,7 +3,7 @@ import 'dart:math';
|
|||
import 'package:aves/geo/countries.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/entry/extensions/location.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/metadata/address.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/analysis_controller.dart';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/utils/collection_utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/utils/collection_utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/dynamic_albums.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/entry/origins.dart';
|
||||
import 'package:aves/model/favourites.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/analysis_controller.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
|
@ -44,7 +45,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
await reportService.log('$runtimeType init target scope=$scope');
|
||||
_essentialLoader ??= _loadEssentials();
|
||||
await _essentialLoader;
|
||||
addDirectories(albums: settings.pinnedFilters.whereType<AlbumFilter>().map((v) => v.album).toSet());
|
||||
addDirectories(albums: settings.pinnedFilters.whereType<StoredAlbumFilter>().map((v) => v.album).toSet());
|
||||
await updateGeneration();
|
||||
unawaited(_loadEntries(
|
||||
analysisController: analysisController,
|
||||
|
@ -59,6 +60,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
await vaults.init();
|
||||
await favourites.init();
|
||||
await covers.init();
|
||||
await dynamicAlbums.init();
|
||||
|
||||
final deviceOffset = DateTime.now().timeZoneOffset.inMilliseconds;
|
||||
final catalogOffset = settings.catalogTimeZoneOffsetMillis;
|
||||
|
@ -81,7 +83,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
state = SourceState.loading;
|
||||
clearEntries();
|
||||
|
||||
final scopeAlbumFilters = _targetScope?.whereType<AlbumFilter>();
|
||||
final scopeAlbumFilters = _targetScope?.whereType<StoredAlbumFilter>();
|
||||
final scopeDirectory = scopeAlbumFilters != null && scopeAlbumFilters.length == 1 ? scopeAlbumFilters.first.album : null;
|
||||
|
||||
final Set<AvesEntry> topEntries = {};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/entry/extensions/catalog.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/filters/covered/tag.dart';
|
||||
import 'package:aves/model/metadata/catalog.dart';
|
||||
import 'package:aves/model/source/analysis_controller.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
|
|
|
@ -126,6 +126,7 @@ class AIcons {
|
|||
static final unpin = MdiIcons.pinOffOutline;
|
||||
static const print = Icons.print_outlined;
|
||||
static const refresh = Icons.refresh_outlined;
|
||||
static const remove = Icons.remove_outlined;
|
||||
static final resetBounds = MdiIcons.rayStartEnd;
|
||||
static const reverse = Icons.invert_colors_outlined;
|
||||
static const reset = Icons.restart_alt_outlined;
|
||||
|
@ -187,6 +188,7 @@ class AIcons {
|
|||
|
||||
// albums
|
||||
static const album = Icons.photo_album_outlined;
|
||||
static const dynamicAlbum = Icons.image_search_outlined;
|
||||
static const cameraAlbum = Icons.photo_camera_outlined;
|
||||
static const downloadAlbum = Icons.file_download;
|
||||
static const screenshotAlbum = Icons.screenshot_outlined;
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
|
||||
|
||||
extension ExtraList<E> on List<E> {
|
||||
bool replace(E old, E newItem) {
|
||||
final index = indexOf(old);
|
||||
|
@ -10,6 +8,15 @@ extension ExtraList<E> on List<E> {
|
|||
}
|
||||
}
|
||||
|
||||
extension ExtraSet<E> on Set<E> {
|
||||
bool replace(E old, E newItem) {
|
||||
if (!remove(old)) return false;
|
||||
|
||||
add(newItem);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
extension ExtraMapNullableKey<K extends Object, V> on Map<K?, V> {
|
||||
Map<K, V> whereNotNullKey() => <K, V>{for (var v in keys.nonNulls) v: this[v] as V};
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ extension ExtraChipSetActionView on ChipSetAction {
|
|||
ChipSetAction.stats => l10n.menuActionStats,
|
||||
// selecting (single/multiple filters)
|
||||
ChipSetAction.delete => l10n.chipActionDelete,
|
||||
ChipSetAction.remove => l10n.chipActionRemove,
|
||||
ChipSetAction.hide => l10n.chipActionHide,
|
||||
ChipSetAction.pin => l10n.chipActionPin,
|
||||
ChipSetAction.unpin => l10n.chipActionUnpin,
|
||||
|
@ -60,6 +61,7 @@ extension ExtraChipSetActionView on ChipSetAction {
|
|||
ChipSetAction.stats => AIcons.stats,
|
||||
// selecting (single/multiple filters)
|
||||
ChipSetAction.delete => AIcons.delete,
|
||||
ChipSetAction.remove => AIcons.remove,
|
||||
ChipSetAction.hide => AIcons.hide,
|
||||
ChipSetAction.pin => AIcons.pin,
|
||||
ChipSetAction.unpin => AIcons.unpin,
|
||||
|
|
|
@ -17,6 +17,7 @@ extension ExtraEntrySetActionView on EntrySetAction {
|
|||
EntrySetAction.toggleTitleSearch =>
|
||||
// different data depending on toggle state
|
||||
l10n.collectionActionShowTitleSearch,
|
||||
EntrySetAction.addDynamicAlbum => l10n.collectionActionAddDynamicAlbum,
|
||||
EntrySetAction.addShortcut => l10n.collectionActionAddShortcut,
|
||||
EntrySetAction.setHome => l10n.collectionActionSetHome,
|
||||
EntrySetAction.emptyBin => l10n.collectionActionEmptyBin,
|
||||
|
@ -62,6 +63,7 @@ extension ExtraEntrySetActionView on EntrySetAction {
|
|||
EntrySetAction.toggleTitleSearch =>
|
||||
// different data depending on toggle state
|
||||
AIcons.filter,
|
||||
EntrySetAction.addDynamicAlbum => AIcons.dynamicAlbum,
|
||||
EntrySetAction.addShortcut => AIcons.addShortcut,
|
||||
EntrySetAction.setHome => AIcons.home,
|
||||
EntrySetAction.emptyBin => AIcons.emptyBin,
|
||||
|
|
|
@ -322,7 +322,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
bool canApply(EntrySetAction action) => _actionDelegate.canApply(
|
||||
action,
|
||||
isSelecting: isSelecting,
|
||||
itemCount: collection.entryCount,
|
||||
collection: collection,
|
||||
selectedItemCount: selectedItemCount,
|
||||
);
|
||||
|
||||
|
@ -462,7 +462,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
return selection.selectedItems.expand((entry) => entry.stackedEntries ?? {entry}).toSet();
|
||||
}
|
||||
|
||||
// key is expected by test driver (e.g. 'menu-configureView', 'menu-map')
|
||||
// key is expected by test driver
|
||||
Key _getActionKey(EntrySetAction action) => Key('menu-${action.name}');
|
||||
|
||||
Widget _buildButtonIcon(
|
||||
|
@ -636,6 +636,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
// browsing
|
||||
case EntrySetAction.searchCollection:
|
||||
case EntrySetAction.toggleTitleSearch:
|
||||
case EntrySetAction.addDynamicAlbum:
|
||||
case EntrySetAction.addShortcut:
|
||||
case EntrySetAction.setHome:
|
||||
// browsing or selecting
|
||||
|
|
|
@ -687,7 +687,7 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
|
|||
sectionLayouts.forEach((section) {
|
||||
final directory = (section.sectionKey as EntryAlbumSectionKey).directory;
|
||||
if (directory != null) {
|
||||
final label = source.getAlbumDisplayName(context, directory);
|
||||
final label = source.getStoredAlbumDisplayName(context, directory);
|
||||
crumbs[section.minOffset / maxOffset] = label;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -70,5 +70,5 @@ class CollectionDraggableThumbLabel extends StatelessWidget {
|
|||
|
||||
bool _showAlbumName(BuildContext context, AvesEntry entry) => _hasMultipleSections(context) && entry.directory != null;
|
||||
|
||||
String _getAlbumName(BuildContext context, AvesEntry entry) => context.read<CollectionSource>().getAlbumDisplayName(context, entry.directory!);
|
||||
String _getAlbumName(BuildContext context, AvesEntry entry) => context.read<CollectionSource>().getStoredAlbumDisplayName(context, entry.directory!);
|
||||
}
|
||||
|
|
|
@ -2,13 +2,17 @@ import 'dart:async';
|
|||
|
||||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/dynamic_albums.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/entry/extensions/favourites.dart';
|
||||
import 'package:aves/model/entry/extensions/metadata_edition.dart';
|
||||
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||
import 'package:aves/model/entry/extensions/props.dart';
|
||||
import 'package:aves/model/favourites.dart';
|
||||
import 'package:aves/model/filters/covered/dynamic_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/set_and.dart';
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/model/metadata/date_modifier.dart';
|
||||
import 'package:aves/model/naming_pattern.dart';
|
||||
import 'package:aves/model/query.dart';
|
||||
|
@ -39,7 +43,9 @@ import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart';
|
|||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/convert_entry_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/entry_editors/rename_entry_set_page.dart';
|
||||
import 'package:aves/widgets/dialogs/filter_editors/add_dynamic_album_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/pick_dialogs/location_pick_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/map/map_page.dart';
|
||||
import 'package:aves/widgets/search/search_delegate.dart';
|
||||
import 'package:aves/widgets/stats/stats_page.dart';
|
||||
|
@ -75,28 +81,29 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
return isSelecting && selectedItemCount == itemCount;
|
||||
// browsing
|
||||
case EntrySetAction.searchCollection:
|
||||
return !useTvLayout && appMode.canNavigate && !isSelecting;
|
||||
return appMode.canNavigate && !isSelecting && !useTvLayout;
|
||||
case EntrySetAction.toggleTitleSearch:
|
||||
return !useTvLayout && !isSelecting;
|
||||
return !isSelecting && !useTvLayout;
|
||||
case EntrySetAction.addShortcut:
|
||||
return isMain && !isSelecting && !isTrash && device.canPinShortcut;
|
||||
case EntrySetAction.addDynamicAlbum:
|
||||
case EntrySetAction.setHome:
|
||||
return isMain && !isSelecting && !isTrash && !useTvLayout;
|
||||
case EntrySetAction.emptyBin:
|
||||
return canWrite && isMain && isTrash;
|
||||
return isMain && isTrash && canWrite;
|
||||
// browsing or selecting
|
||||
case EntrySetAction.map:
|
||||
case EntrySetAction.slideshow:
|
||||
case EntrySetAction.stats:
|
||||
return isMain;
|
||||
case EntrySetAction.rescan:
|
||||
return !useTvLayout && isMain && !isTrash && isSelecting;
|
||||
return isMain && isSelecting && !isTrash && !useTvLayout;
|
||||
// selecting
|
||||
case EntrySetAction.share:
|
||||
case EntrySetAction.toggleFavourite:
|
||||
return isMain && isSelecting && !isTrash;
|
||||
case EntrySetAction.delete:
|
||||
return canWrite && isMain && isSelecting;
|
||||
return isMain && isSelecting && canWrite;
|
||||
case EntrySetAction.copy:
|
||||
case EntrySetAction.move:
|
||||
case EntrySetAction.rename:
|
||||
|
@ -110,18 +117,19 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
case EntrySetAction.editRating:
|
||||
case EntrySetAction.editTags:
|
||||
case EntrySetAction.removeMetadata:
|
||||
return canWrite && isMain && isSelecting && !isTrash;
|
||||
return isMain && isSelecting && !isTrash && canWrite;
|
||||
case EntrySetAction.restore:
|
||||
return canWrite && isMain && isSelecting && isTrash;
|
||||
return isMain && isSelecting && isTrash && canWrite;
|
||||
}
|
||||
}
|
||||
|
||||
bool canApply(
|
||||
EntrySetAction action, {
|
||||
required bool isSelecting,
|
||||
required int itemCount,
|
||||
required CollectionLens collection,
|
||||
required int selectedItemCount,
|
||||
}) {
|
||||
final itemCount = collection.entryCount;
|
||||
final hasItems = itemCount > 0;
|
||||
final hasSelection = selectedItemCount > 0;
|
||||
|
||||
|
@ -139,6 +147,8 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
case EntrySetAction.addShortcut:
|
||||
case EntrySetAction.setHome:
|
||||
return true;
|
||||
case EntrySetAction.addDynamicAlbum:
|
||||
return collection.filters.isNotEmpty;
|
||||
case EntrySetAction.emptyBin:
|
||||
return !isSelecting && hasItems;
|
||||
case EntrySetAction.map:
|
||||
|
@ -184,6 +194,8 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
final routeName = context.currentRouteName!;
|
||||
settings.setShowTitleQuery(routeName, !settings.getShowTitleQuery(routeName));
|
||||
context.read<Query>().toggle();
|
||||
case EntrySetAction.addDynamicAlbum:
|
||||
_addDynamicAlbum(context);
|
||||
case EntrySetAction.addShortcut:
|
||||
_addShortcut(context);
|
||||
case EntrySetAction.setHome:
|
||||
|
@ -727,10 +739,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> _addShortcut(BuildContext context) async {
|
||||
final collection = context.read<CollectionLens>();
|
||||
final filters = collection.filters;
|
||||
|
||||
static String? _getDefaultNameForFilters(BuildContext context, Set<CollectionFilter> filters) {
|
||||
String? defaultName;
|
||||
if (filters.isNotEmpty) {
|
||||
// we compute the default name beforehand
|
||||
|
@ -738,6 +747,67 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
final sortedFilters = List<CollectionFilter>.from(filters)..sort();
|
||||
defaultName = sortedFilters.first.getLabel(context).replaceAll('\n', ' ');
|
||||
}
|
||||
return defaultName;
|
||||
}
|
||||
|
||||
Future<void> _addDynamicAlbum(BuildContext context) async {
|
||||
final l10n = context.l10n;
|
||||
final collection = context.read<CollectionLens>();
|
||||
final filters = collection.filters;
|
||||
if (filters.isEmpty) return;
|
||||
|
||||
// get navigator beforehand because
|
||||
// local context may be deactivated when action is triggered after navigation
|
||||
final navigator = Navigator.maybeOf(context);
|
||||
|
||||
final name = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => const AddDynamicAlbumDialog(),
|
||||
routeSettings: const RouteSettings(name: AddDynamicAlbumDialog.routeName),
|
||||
);
|
||||
if (name == null) return;
|
||||
|
||||
final existingAlbum = dynamicAlbums.get(name);
|
||||
if (existingAlbum != null) {
|
||||
// album already exists, so we just need to highlight it
|
||||
await _showDynamicAlbum(navigator, existingAlbum);
|
||||
} else {
|
||||
final album = DynamicAlbumFilter(name, filters.length == 1 ? filters.first : SetAndFilter(filters));
|
||||
await dynamicAlbums.add(album);
|
||||
|
||||
final showAction = SnackBarAction(
|
||||
label: l10n.showButtonLabel,
|
||||
onPressed: () => _showDynamicAlbum(navigator, album),
|
||||
);
|
||||
showFeedback(context, FeedbackType.info, l10n.genericSuccessFeedback, showAction);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showDynamicAlbum(NavigatorState? navigator, DynamicAlbumFilter album) async {
|
||||
// local context may be deactivated when action is triggered after navigation
|
||||
if (navigator != null) {
|
||||
final context = navigator.context;
|
||||
final highlightInfo = context.read<HighlightInfo>();
|
||||
if (context.currentRouteName == AlbumListPage.routeName) {
|
||||
highlightInfo.trackItem(FilterGridItem(album, null), highlightItem: album);
|
||||
} else {
|
||||
highlightInfo.set(album);
|
||||
await navigator.pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: AlbumListPage.routeName),
|
||||
builder: (_) => const AlbumListPage(),
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _addShortcut(BuildContext context) async {
|
||||
final collection = context.read<CollectionLens>();
|
||||
final filters = collection.filters;
|
||||
|
||||
String? defaultName = _getDefaultNameForFilters(context, filters);
|
||||
final result = await showDialog<(AvesEntry?, String)>(
|
||||
context: context,
|
||||
builder: (context) => AddShortcutDialog(
|
||||
|
|
|
@ -53,7 +53,7 @@ class AlbumSectionHeader extends StatelessWidget {
|
|||
return SectionHeader.getPreferredHeight(
|
||||
context: context,
|
||||
maxWidth: maxWidth,
|
||||
title: source.getAlbumDisplayName(context, directory),
|
||||
title: source.getStoredAlbumDisplayName(context, directory),
|
||||
hasLeading: covers.effectiveAlbumType(directory) != AlbumType.regular,
|
||||
hasTrailing: androidFileUtils.isOnRemovableStorage(directory),
|
||||
);
|
||||
|
|
|
@ -78,7 +78,7 @@ class CollectionSectionHeader extends StatelessWidget {
|
|||
return AlbumSectionHeader(
|
||||
key: ValueKey(sectionKey),
|
||||
directory: directory,
|
||||
albumName: directory != null ? source.getAlbumDisplayName(context, directory) : null,
|
||||
albumName: directory != null ? source.getStoredAlbumDisplayName(context, directory) : null,
|
||||
selectable: selectable,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/widgets/common/action_controls/quick_choosers/common/menu.dart';
|
||||
|
@ -46,6 +46,6 @@ class AlbumQuickChooser extends StatelessWidget with FilterQuickChooserMixin<Str
|
|||
@override
|
||||
CollectionFilter buildFilter(BuildContext context, String option) {
|
||||
final source = context.read<CollectionSource>();
|
||||
return AlbumFilter(option, source.getAlbumDisplayName(context, option));
|
||||
return StoredAlbumFilter(option, source.getStoredAlbumDisplayName(context, option));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
|
@ -44,7 +44,7 @@ class _MoveButtonState extends ChooserQuickButtonState<MoveButton, String> {
|
|||
final options = settings.recentDestinationAlbums.where(rawAlbums.contains).toList();
|
||||
final takeCount = FilterQuickChooserMixin.maxTotalOptionCount - options.length;
|
||||
if (takeCount > 0) {
|
||||
final filters = rawAlbums.whereNot(options.contains).map((album) => AlbumFilter(album, null)).toSet();
|
||||
final filters = rawAlbums.whereNot(options.contains).map((album) => StoredAlbumFilter(album, null)).toSet();
|
||||
final allMapEntries = filters.map((filter) => FilterGridItem(filter, source.recentEntry(filter))).toList();
|
||||
allMapEntries.sort(FilterNavigationPage.compareFiltersByDate);
|
||||
options.addAll(allMapEntries.take(takeCount).map((v) => v.filter.album));
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/filters/covered/tag.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/view/view.dart';
|
||||
|
|
|
@ -3,7 +3,7 @@ import 'package:aves/model/entry/extensions/metadata_edition.dart';
|
|||
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/placeholder.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/filters/covered/tag.dart';
|
||||
import 'package:aves/model/metadata/date_modifier.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
|
|
|
@ -5,7 +5,7 @@ import 'package:aves/app_mode.dart';
|
|||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||
import 'package:aves/model/entry/extensions/props.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/trash.dart';
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/model/metadata/date_modifier.dart';
|
||||
|
@ -38,8 +38,10 @@ import 'package:provider/provider.dart';
|
|||
|
||||
mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
||||
Future<void> doExport(BuildContext context, Set<AvesEntry> targetEntries, EntryConvertOptions options) async {
|
||||
final destinationAlbum = await pickAlbum(context: context, moveType: MoveType.export);
|
||||
if (destinationAlbum == null) return;
|
||||
final destinationAlbumFilter = await pickAlbum(context: context, moveType: MoveType.export, storedAlbumsOnly: true);
|
||||
if (destinationAlbumFilter == null || destinationAlbumFilter is! StoredAlbumFilter) return;
|
||||
|
||||
final destinationAlbum = destinationAlbumFilter.album;
|
||||
if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return;
|
||||
|
||||
if (!await checkFreeSpaceForMove(context, targetEntries, destinationAlbum, MoveType.export)) return;
|
||||
|
@ -125,7 +127,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
|||
settings: const RouteSettings(name: CollectionPage.routeName),
|
||||
builder: (context) => CollectionPage(
|
||||
source: source,
|
||||
filters: {AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum))},
|
||||
filters: {StoredAlbumFilter(destinationAlbum, source.getStoredAlbumDisplayName(context, destinationAlbum))},
|
||||
highlightTest: (entry) => newUris.contains(entry.uri),
|
||||
),
|
||||
),
|
||||
|
@ -337,9 +339,10 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
|||
case MoveType.copy:
|
||||
case MoveType.move:
|
||||
case MoveType.export:
|
||||
final destinationAlbum = await pickAlbum(context: context, moveType: moveType);
|
||||
if (destinationAlbum == null) return;
|
||||
final destinationAlbumFilter = await pickAlbum(context: context, moveType: moveType, storedAlbumsOnly: true);
|
||||
if (destinationAlbumFilter == null || destinationAlbumFilter is! StoredAlbumFilter) return;
|
||||
|
||||
final destinationAlbum = destinationAlbumFilter.album;
|
||||
settings.recentDestinationAlbums = settings.recentDestinationAlbums
|
||||
..remove(destinationAlbum)
|
||||
..insert(0, destinationAlbum);
|
||||
|
@ -452,15 +455,15 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
|||
bool highlightTest(AvesEntry entry) => newUris.contains(entry.uri);
|
||||
|
||||
final collection = context.read<CollectionLens?>();
|
||||
if (collection == null || collection.filters.any((f) => f is AlbumFilter || f is TrashFilter)) {
|
||||
if (collection == null || collection.filters.any((f) => f is StoredAlbumFilter || f is TrashFilter)) {
|
||||
final source = context.read<CollectionSource>();
|
||||
final targetFilters = collection?.filters.where((f) => f != TrashFilter.instance).toSet() ?? {};
|
||||
// we could simply add the filter to the current collection
|
||||
// but navigating makes the change less jarring
|
||||
if (destinationAlbums.length == 1) {
|
||||
final destinationAlbum = destinationAlbums.single;
|
||||
targetFilters.removeWhere((f) => f is AlbumFilter);
|
||||
targetFilters.add(AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum)));
|
||||
targetFilters.removeWhere((f) => f is StoredAlbumFilter);
|
||||
targetFilters.add(StoredAlbumFilter(destinationAlbum, source.getStoredAlbumDisplayName(context, destinationAlbum)));
|
||||
}
|
||||
unawaited(Navigator.maybeOf(context)?.pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/vaults/details.dart';
|
||||
import 'package:aves/model/vaults/vaults.dart';
|
||||
|
@ -80,10 +80,10 @@ mixin VaultAwareMixin on FeedbackMixin {
|
|||
}
|
||||
|
||||
Future<bool> unlockFilter(BuildContext context, CollectionFilter filter) {
|
||||
return filter is AlbumFilter ? unlockAlbum(context, filter.album) : Future.value(true);
|
||||
return filter is StoredAlbumFilter ? unlockAlbum(context, filter.album) : Future.value(true);
|
||||
}
|
||||
|
||||
Future<bool> unlockFilters(BuildContext context, Set<AlbumFilter> filters) async {
|
||||
Future<bool> unlockFilters(BuildContext context, Set<StoredAlbumFilter> filters) async {
|
||||
var unlocked = true;
|
||||
await Future.forEach(filters, (filter) async {
|
||||
if (unlocked) {
|
||||
|
@ -93,7 +93,7 @@ mixin VaultAwareMixin on FeedbackMixin {
|
|||
return unlocked;
|
||||
}
|
||||
|
||||
void lockFilters(Set<AlbumFilter> filters) => vaults.lock(filters.map((v) => v.album).toSet());
|
||||
void lockFilters(Set<StoredAlbumFilter> filters) => vaults.lock(filters.map((v) => v.album).toSet());
|
||||
|
||||
Future<bool> setVaultPass(BuildContext context, VaultDetails details) async {
|
||||
switch (details.lockType) {
|
||||
|
|
|
@ -167,7 +167,7 @@ class ExpandableFilterRow extends StatelessWidget {
|
|||
|
||||
Widget _buildChip(CollectionFilter filter) {
|
||||
return AvesFilterChip(
|
||||
// key `album-{path}` is expected by test driver
|
||||
// key is expected by test driver
|
||||
key: Key(filter.key),
|
||||
filter: filter,
|
||||
allowGenericIcon: showGenericIcon,
|
||||
|
|
|
@ -3,12 +3,12 @@ import 'dart:math';
|
|||
|
||||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/filters/path.dart';
|
||||
import 'package:aves/model/filters/rating.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/filters/covered/tag.dart';
|
||||
import 'package:aves/model/settings/enums/accessibility_animations.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/colors.dart';
|
||||
|
@ -102,14 +102,11 @@ class AvesFilterChip extends StatefulWidget {
|
|||
static Future<void> showDefaultLongPressMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async {
|
||||
if (context.read<ValueNotifier<AppMode>>().value.canNavigate) {
|
||||
final actions = <ChipAction>[
|
||||
if (filter is AlbumFilter) ...[
|
||||
ChipAction.goToAlbumPage,
|
||||
ChipAction.goToExplorerPage,
|
||||
],
|
||||
if (filter is AlbumBaseFilter) ChipAction.goToAlbumPage,
|
||||
if (filter is StoredAlbumFilter || filter is PathFilter) ChipAction.goToExplorerPage,
|
||||
if ((filter is LocationFilter && filter.level == LocationLevel.country)) ChipAction.goToCountryPage,
|
||||
if ((filter is LocationFilter && filter.level == LocationLevel.place)) ChipAction.goToPlacePage,
|
||||
if (filter is TagFilter) ChipAction.goToTagPage,
|
||||
if (filter is PathFilter) ChipAction.goToExplorerPage,
|
||||
if (filter is RatingFilter && 1 < filter.rating && filter.rating < 5) ...[
|
||||
if (filter.op != RatingFilter.opOrGreater) ChipAction.ratingOrGreater,
|
||||
if (filter.op != RatingFilter.opOrLower) ChipAction.ratingOrLower,
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/favourites.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/filters/path.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/filters/covered/tag.dart';
|
||||
import 'package:aves/model/settings/enums/accessibility_animations.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/dynamic_albums.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/favourites.dart';
|
||||
import 'package:aves/model/metadata/address.dart';
|
||||
|
@ -31,6 +32,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
|
|||
late Future<Set<VaultDetails>> _dbVaultsLoader;
|
||||
late Future<Set<FavouriteRow>> _dbFavouritesLoader;
|
||||
late Future<Set<CoverRow>> _dbCoversLoader;
|
||||
late Future<Set<DynamicAlbumRow>> _dbDynamicAlbumsLoader;
|
||||
late Future<Set<VideoPlaybackRow>> _dbVideoPlaybackLoader;
|
||||
|
||||
@override
|
||||
|
@ -247,6 +249,27 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
|
|||
);
|
||||
},
|
||||
),
|
||||
FutureBuilder<Set>(
|
||||
future: _dbDynamicAlbumsLoader,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) return Text(snapshot.error.toString());
|
||||
|
||||
if (snapshot.connectionState != ConnectionState.done) return const SizedBox();
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text('dynamic album rows: ${snapshot.data!.length} (${dynamicAlbums.count} in memory)'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () => dynamicAlbums.clear().then((_) => _reload()),
|
||||
child: const Text('Clear'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
FutureBuilder<Set>(
|
||||
future: _dbVideoPlaybackLoader,
|
||||
builder: (context, snapshot) {
|
||||
|
@ -290,6 +313,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
|
|||
_dbVaultsLoader = localMediaDb.loadAllVaults();
|
||||
_dbFavouritesLoader = localMediaDb.loadAllFavourites();
|
||||
_dbCoversLoader = localMediaDb.loadAllCovers();
|
||||
_dbDynamicAlbumsLoader = localMediaDb.loadAllDynamicAlbums();
|
||||
_dbVideoPlaybackLoader = localMediaDb.loadAllVideoPlayback();
|
||||
setState(() {});
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import 'dart:async';
|
|||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/entry/extensions/location.dart';
|
||||
import 'package:aves/model/entry/extensions/metadata_edition.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/settings/enums/coordinate_format.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/placeholder.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/filters/covered/tag.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
import 'package:aves/model/dynamic_albums.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AddDynamicAlbumDialog extends StatefulWidget {
|
||||
static const routeName = '/dialog/add_dynamic_album';
|
||||
|
||||
const AddDynamicAlbumDialog({super.key});
|
||||
|
||||
@override
|
||||
State<AddDynamicAlbumDialog> createState() => _AddDynamicAlbumDialogState();
|
||||
}
|
||||
|
||||
class _AddDynamicAlbumDialogState extends State<AddDynamicAlbumDialog> {
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final ValueNotifier<bool> _existsNotifier = ValueNotifier(false);
|
||||
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_validate();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_existsNotifier.dispose();
|
||||
_isValidNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
return AvesDialog(
|
||||
title: l10n.newDynamicAlbumDialogTitle,
|
||||
content: ValueListenableBuilder<bool>(
|
||||
valueListenable: _existsNotifier,
|
||||
builder: (context, exists, child) {
|
||||
return TextField(
|
||||
controller: _nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.newAlbumDialogNameLabel,
|
||||
helperText: exists ? l10n.dynamicAlbumAlreadyExists : '',
|
||||
),
|
||||
autofocus: true,
|
||||
onChanged: (_) => _validate(),
|
||||
onSubmitted: (_) => _submit(context),
|
||||
);
|
||||
}),
|
||||
actions: [
|
||||
const CancelButton(),
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: _existsNotifier,
|
||||
builder: (context, albumExists, child) {
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: _isValidNotifier,
|
||||
builder: (context, isValid, child) {
|
||||
return TextButton(
|
||||
onPressed: isValid ? () => _submit(context) : null,
|
||||
child: Text(albumExists ? l10n.showButtonLabel : l10n.createAlbumButtonLabel),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String? _formatAlbumName() {
|
||||
final name = _nameController.text.trim();
|
||||
if (name.isEmpty) return null;
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
void _validate() {
|
||||
final name = _formatAlbumName();
|
||||
final isValid = name != null;
|
||||
_isValidNotifier.value = isValid;
|
||||
_existsNotifier.value = isValid && dynamicAlbums.contains(name);
|
||||
}
|
||||
|
||||
void _submit(BuildContext context) {
|
||||
if (_isValidNotifier.value) {
|
||||
Navigator.maybeOf(context)?.pop(_formatAlbumName());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ import 'dart:math';
|
|||
|
||||
import 'package:aves/image_providers/app_icon_image_provider.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
|
@ -49,7 +49,7 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
|
|||
|
||||
CollectionFilter get filter => widget.filter;
|
||||
|
||||
bool get showAppTab => filter is AlbumFilter && settings.isInstalledAppAccessAllowed;
|
||||
bool get showAppTab => filter is StoredAlbumFilter && settings.isInstalledAppAccessAllowed;
|
||||
|
||||
bool get showColorTab => settings.themeColorMode == AvesThemeColorMode.polychrome;
|
||||
|
||||
|
@ -205,32 +205,35 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
|
|||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
);
|
||||
return RadioListTile<bool>(
|
||||
value: isCustom,
|
||||
groupValue: _isCustomEntry,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
if (v && _customEntry == null) {
|
||||
_pickEntry();
|
||||
return;
|
||||
}
|
||||
_isCustomEntry = v;
|
||||
setState(() {});
|
||||
},
|
||||
title: isCustom
|
||||
? Row(
|
||||
children: [
|
||||
title,
|
||||
const Spacer(),
|
||||
if (_customEntry != null)
|
||||
ItemPicker(
|
||||
extent: itemPickerExtent,
|
||||
entry: _customEntry!,
|
||||
onTap: _pickEntry,
|
||||
),
|
||||
],
|
||||
)
|
||||
: title,
|
||||
return ListTileTheme.merge(
|
||||
minVerticalPadding: isCustom && _customEntry != null ? 0 : null,
|
||||
child: RadioListTile<bool>(
|
||||
value: isCustom,
|
||||
groupValue: _isCustomEntry,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
if (v && _customEntry == null) {
|
||||
_pickEntry();
|
||||
return;
|
||||
}
|
||||
_isCustomEntry = v;
|
||||
setState(() {});
|
||||
},
|
||||
title: isCustom
|
||||
? Row(
|
||||
children: [
|
||||
title,
|
||||
const Spacer(),
|
||||
if (_customEntry != null)
|
||||
ItemPicker(
|
||||
extent: itemPickerExtent,
|
||||
entry: _customEntry!,
|
||||
onTap: _pickEntry,
|
||||
),
|
||||
],
|
||||
)
|
||||
: title,
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList();
|
||||
|
|
|
@ -12,16 +12,16 @@ import 'package:collection/collection.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class CreateAlbumDialog extends StatefulWidget {
|
||||
static const routeName = '/dialog/create_album';
|
||||
class CreateStoredAlbumDialog extends StatefulWidget {
|
||||
static const routeName = '/dialog/create_stored_album';
|
||||
|
||||
const CreateAlbumDialog({super.key});
|
||||
const CreateStoredAlbumDialog({super.key});
|
||||
|
||||
@override
|
||||
State<CreateAlbumDialog> createState() => _CreateAlbumDialogState();
|
||||
State<CreateStoredAlbumDialog> createState() => _CreateStoredAlbumDialogState();
|
||||
}
|
||||
|
||||
class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
||||
class _CreateStoredAlbumDialogState extends State<CreateStoredAlbumDialog> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final FocusNode _nameFieldFocusNode = FocusNode();
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/vaults/details.dart';
|
||||
|
@ -127,7 +127,7 @@ class _EditVaultDialogState extends State<EditVaultDialog> with FeedbackMixin, V
|
|||
if (!v) {
|
||||
final album = initialDetails?.path;
|
||||
if (album != null) {
|
||||
final filter = AlbumFilter(album, null);
|
||||
final filter = StoredAlbumFilter(album, null);
|
||||
final source = context.read<CollectionSource>();
|
||||
if (source.trashedEntries.any(filter.test)) {
|
||||
if (!await showConfirmationDialog(
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
import 'package:aves/model/dynamic_albums.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class RenameDynamicAlbumDialog extends StatefulWidget {
|
||||
static const routeName = '/dialog/rename_dynamic_album';
|
||||
|
||||
final String name;
|
||||
|
||||
const RenameDynamicAlbumDialog({
|
||||
super.key,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RenameDynamicAlbumDialog> createState() => _RenameDynamicAlbumDialogState();
|
||||
}
|
||||
|
||||
class _RenameDynamicAlbumDialogState extends State<RenameDynamicAlbumDialog> {
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final ValueNotifier<bool> _existsNotifier = ValueNotifier(false);
|
||||
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
||||
|
||||
String get initialValue => widget.name;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_nameController.text = initialValue;
|
||||
_validate();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_existsNotifier.dispose();
|
||||
_isValidNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AvesDialog(
|
||||
content: ValueListenableBuilder<bool>(
|
||||
valueListenable: _existsNotifier,
|
||||
builder: (context, exists, child) {
|
||||
return TextField(
|
||||
controller: _nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: context.l10n.renameAlbumDialogLabel,
|
||||
helperText: exists ? context.l10n.dynamicAlbumAlreadyExists : '',
|
||||
),
|
||||
autofocus: true,
|
||||
onChanged: (_) => _validate(),
|
||||
onSubmitted: (_) => _submit(context),
|
||||
);
|
||||
}),
|
||||
actions: [
|
||||
const CancelButton(),
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: _isValidNotifier,
|
||||
builder: (context, isValid, child) {
|
||||
return TextButton(
|
||||
onPressed: isValid ? () => _submit(context) : null,
|
||||
child: Text(context.l10n.applyButtonLabel),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String? _formatAlbumName() {
|
||||
final name = _nameController.text.trim();
|
||||
if (name.isEmpty) return null;
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
Future<void> _validate() async {
|
||||
final newName = _formatAlbumName();
|
||||
_isValidNotifier.value = newName != null && !dynamicAlbums.contains(newName);
|
||||
_existsNotifier.value = newName != null && dynamicAlbums.contains(newName) && newName != initialValue;
|
||||
}
|
||||
|
||||
void _submit(BuildContext context) {
|
||||
if (_isValidNotifier.value) {
|
||||
Navigator.maybeOf(context)?.pop(_formatAlbumName());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,21 +5,21 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
|||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class RenameAlbumDialog extends StatefulWidget {
|
||||
static const routeName = '/dialog/rename_album';
|
||||
class RenameStoredAlbumDialog extends StatefulWidget {
|
||||
static const routeName = '/dialog/rename_stored_album';
|
||||
|
||||
final String album;
|
||||
|
||||
const RenameAlbumDialog({
|
||||
const RenameStoredAlbumDialog({
|
||||
super.key,
|
||||
required this.album,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RenameAlbumDialog> createState() => _RenameAlbumDialogState();
|
||||
State<RenameStoredAlbumDialog> createState() => _RenameStoredAlbumDialogState();
|
||||
}
|
||||
|
||||
class _RenameAlbumDialogState extends State<RenameAlbumDialog> {
|
||||
class _RenameStoredAlbumDialogState extends State<RenameStoredAlbumDialog> {
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final ValueNotifier<bool> _existsNotifier = ValueNotifier(false);
|
||||
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/selection.dart';
|
||||
import 'package:aves/model/settings/enums/accessibility_animations.dart';
|
||||
|
@ -19,7 +19,7 @@ import 'package:aves/widgets/common/identity/empty.dart';
|
|||
import 'package:aves/widgets/common/providers/query_provider.dart';
|
||||
import 'package:aves/widgets/common/providers/selection_provider.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/filter_editors/create_album_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/filter_editors/create_stored_album_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/filter_editors/edit_vault_dialog.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/album_set.dart';
|
||||
|
@ -30,9 +30,10 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
Future<String?> pickAlbum({
|
||||
Future<AlbumBaseFilter?> pickAlbum({
|
||||
required BuildContext context,
|
||||
required MoveType? moveType,
|
||||
required bool storedAlbumsOnly,
|
||||
}) async {
|
||||
final source = context.read<CollectionSource>();
|
||||
if (source.targetScope != CollectionSource.fullScope) {
|
||||
|
@ -41,13 +42,12 @@ Future<String?> pickAlbum({
|
|||
source.canAnalyze = true;
|
||||
await source.init(scope: CollectionSource.fullScope);
|
||||
}
|
||||
final filter = await Navigator.maybeOf(context)?.push(
|
||||
MaterialPageRoute<AlbumFilter>(
|
||||
return await Navigator.maybeOf(context)?.push(
|
||||
MaterialPageRoute<AlbumBaseFilter>(
|
||||
settings: const RouteSettings(name: _AlbumPickPage.routeName),
|
||||
builder: (context) => _AlbumPickPage(source: source, moveType: moveType),
|
||||
builder: (context) => _AlbumPickPage(source: source, moveType: moveType, storedAlbumsOnly: storedAlbumsOnly),
|
||||
),
|
||||
);
|
||||
return filter?.album;
|
||||
}
|
||||
|
||||
class _AlbumPickPage extends StatefulWidget {
|
||||
|
@ -55,10 +55,12 @@ class _AlbumPickPage extends StatefulWidget {
|
|||
|
||||
final CollectionSource source;
|
||||
final MoveType? moveType;
|
||||
final bool storedAlbumsOnly;
|
||||
|
||||
const _AlbumPickPage({
|
||||
required this.source,
|
||||
required this.moveType,
|
||||
required this.storedAlbumsOnly,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -111,11 +113,11 @@ class _AlbumPickPageState extends State<_AlbumPickPage> {
|
|||
return StreamBuilder(
|
||||
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
||||
builder: (context, snapshot) {
|
||||
final gridItems = AlbumListPage.getAlbumGridItems(context, source);
|
||||
return SelectionProvider<FilterGridItem<AlbumFilter>>(
|
||||
final gridItems = AlbumListPage.getAlbumGridItems(context, source, storedAlbumsOnly: widget.storedAlbumsOnly);
|
||||
return SelectionProvider<FilterGridItem<AlbumBaseFilter>>(
|
||||
child: QueryProvider(
|
||||
startEnabled: settings.getShowTitleQuery(context.currentRouteName!),
|
||||
child: FilterGridPage<AlbumFilter>(
|
||||
child: FilterGridPage<AlbumBaseFilter>(
|
||||
settingsRouteKey: AlbumListPage.routeName,
|
||||
appBar: FilterGridAppBar(
|
||||
source: source,
|
||||
|
@ -150,7 +152,7 @@ class _AlbumPickPageState extends State<_AlbumPickPage> {
|
|||
List<Widget> _buildActions(
|
||||
BuildContext context,
|
||||
AppMode appMode,
|
||||
Selection<FilterGridItem<AlbumFilter>> selection,
|
||||
Selection<FilterGridItem<AlbumBaseFilter>> selection,
|
||||
AlbumChipSetActionDelegate actionDelegate,
|
||||
) {
|
||||
final itemCount = actionDelegate.allItems.length;
|
||||
|
@ -245,8 +247,8 @@ class _AlbumPickPageState extends State<_AlbumPickPage> {
|
|||
Future<void> _createAlbum() async {
|
||||
final directory = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => const CreateAlbumDialog(),
|
||||
routeSettings: const RouteSettings(name: CreateAlbumDialog.routeName),
|
||||
builder: (context) => const CreateStoredAlbumDialog(),
|
||||
routeSettings: const RouteSettings(name: CreateStoredAlbumDialog.routeName),
|
||||
);
|
||||
if (directory == null) return;
|
||||
|
||||
|
@ -282,8 +284,8 @@ class _AlbumPickPageState extends State<_AlbumPickPage> {
|
|||
}
|
||||
|
||||
void _pickAlbum(String directory) {
|
||||
source.createAlbum(directory);
|
||||
final filter = AlbumFilter(directory, source.getAlbumDisplayName(context, directory));
|
||||
Navigator.maybeOf(context)?.pop<AlbumFilter>(filter);
|
||||
source.createStoredAlbum(directory);
|
||||
final filter = StoredAlbumFilter(directory, source.getStoredAlbumDisplayName(context, directory));
|
||||
Navigator.maybeOf(context)?.pop<StoredAlbumFilter>(filter);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,9 +16,9 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
|||
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
|
||||
import 'package:aves/widgets/common/search/route.dart';
|
||||
import 'package:aves/widgets/dialogs/select_storage_dialog.dart';
|
||||
import 'package:aves/widgets/explorer/crumb_line.dart';
|
||||
import 'package:aves/widgets/explorer/explorer_action_delegate.dart';
|
||||
import 'package:aves/widgets/search/search_delegate.dart';
|
||||
import 'package:aves/widgets/settings/privacy/file_picker/crumb_line.dart';
|
||||
import 'package:aves_model/aves_model.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/path.dart';
|
||||
import 'package:aves/model/source/album.dart';
|
||||
|
@ -194,7 +194,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
final album = _getAlbumPath(source, Directory(dirPath));
|
||||
if (album != null) {
|
||||
bottom = AvesFilterChip(
|
||||
filter: AlbumFilter(album, source.getAlbumDisplayName(context, album)),
|
||||
filter: StoredAlbumFilter(album, source.getStoredAlbumDisplayName(context, album)),
|
||||
maxWidth: double.infinity,
|
||||
onTap: (filter) => _goToCollectionPage(context, filter),
|
||||
onLongPress: null,
|
||||
|
@ -237,7 +237,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
? IconTheme.merge(
|
||||
data: baseIconTheme,
|
||||
child: AvesFilterChip(
|
||||
filter: AlbumFilter(album, source.getAlbumDisplayName(context, album)),
|
||||
filter: StoredAlbumFilter(album, source.getStoredAlbumDisplayName(context, album)),
|
||||
showText: false,
|
||||
maxWidth: leadingDim,
|
||||
onTap: (filter) => _goToCollectionPage(context, filter),
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import 'package:aves/model/app_inventory.dart';
|
||||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/dynamic_albums.dart';
|
||||
import 'package:aves/model/entry/extensions/props.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/covered/dynamic_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/album.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/empty.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/album_set.dart';
|
||||
|
@ -37,29 +38,32 @@ class AlbumListPage extends StatelessWidget {
|
|||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: appInventory.areAppNamesReadyNotifier,
|
||||
builder: (context, areAppNamesReady, child) {
|
||||
return StreamBuilder(
|
||||
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
||||
builder: (context, snapshot) {
|
||||
final gridItems = getAlbumGridItems(context, source);
|
||||
return StreamBuilder<Set<CollectionFilter>?>(
|
||||
// to update sections by tier
|
||||
stream: covers.packageChangeStream,
|
||||
builder: (context, snapshot) => FilterNavigationPage<AlbumFilter, AlbumChipSetActionDelegate>(
|
||||
source: source,
|
||||
title: context.l10n.albumPageTitle,
|
||||
sortFactor: settings.albumSortFactor,
|
||||
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none,
|
||||
actionDelegate: AlbumChipSetActionDelegate(gridItems),
|
||||
filterSections: groupToSections(context, source, gridItems),
|
||||
newFilters: source.getNewAlbumFilters(context),
|
||||
applyQuery: applyQuery,
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.album,
|
||||
text: context.l10n.albumEmpty,
|
||||
return AnimatedBuilder(
|
||||
animation: dynamicAlbums,
|
||||
builder: (context, child) => StreamBuilder(
|
||||
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
||||
builder: (context, snapshot) {
|
||||
final gridItems = getAlbumGridItems(context, source);
|
||||
return StreamBuilder<Set<CollectionFilter>?>(
|
||||
// to update sections by tier
|
||||
stream: covers.packageChangeStream,
|
||||
builder: (context, snapshot) => FilterNavigationPage<AlbumBaseFilter, AlbumChipSetActionDelegate>(
|
||||
source: source,
|
||||
title: context.l10n.albumPageTitle,
|
||||
sortFactor: settings.albumSortFactor,
|
||||
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none,
|
||||
actionDelegate: AlbumChipSetActionDelegate(gridItems),
|
||||
filterSections: groupToSections(context, source, gridItems),
|
||||
newFilters: source.getNewAlbumFilters(context),
|
||||
applyQuery: applyQuery,
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.album,
|
||||
text: context.l10n.albumEmpty,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -69,21 +73,24 @@ class AlbumListPage extends StatelessWidget {
|
|||
|
||||
// common with album selection page to move/copy entries
|
||||
|
||||
static List<FilterGridItem<AlbumFilter>> applyQuery(BuildContext context, List<FilterGridItem<AlbumFilter>> filters, String query) {
|
||||
return filters.where((item) => (item.filter.displayName ?? item.filter.album).toUpperCase().contains(query)).toList();
|
||||
static List<FilterGridItem<AlbumBaseFilter>> applyQuery(BuildContext context, List<FilterGridItem<AlbumBaseFilter>> filters, String query) {
|
||||
return filters.where((item) => item.filter.match(query)).toList();
|
||||
}
|
||||
|
||||
static List<FilterGridItem<AlbumFilter>> getAlbumGridItems(BuildContext context, CollectionSource source) {
|
||||
final filters = source.rawAlbums.map((album) => AlbumFilter(album, source.getAlbumDisplayName(context, album))).toSet();
|
||||
static List<FilterGridItem<AlbumBaseFilter>> getAlbumGridItems(BuildContext context, CollectionSource source, {bool storedAlbumsOnly = false}) {
|
||||
final filters = <AlbumBaseFilter>{
|
||||
...source.rawAlbums.map((album) => StoredAlbumFilter(album, source.getStoredAlbumDisplayName(context, album))),
|
||||
if (!storedAlbumsOnly) ...dynamicAlbums.all,
|
||||
};
|
||||
|
||||
return FilterNavigationPage.sort(settings.albumSortFactor, settings.albumSortReverse, source, filters);
|
||||
}
|
||||
|
||||
static Map<ChipSectionKey, List<FilterGridItem<AlbumFilter>>> groupToSections(BuildContext context, CollectionSource source, Iterable<FilterGridItem<AlbumFilter>> sortedMapEntries) {
|
||||
static Map<ChipSectionKey, List<FilterGridItem<AlbumBaseFilter>>> groupToSections(BuildContext context, CollectionSource source, Iterable<FilterGridItem<AlbumBaseFilter>> sortedMapEntries) {
|
||||
final newFilters = source.getNewAlbumFilters(context);
|
||||
final pinned = settings.pinnedFilters.whereType<AlbumFilter>();
|
||||
final pinned = settings.pinnedFilters.whereType<AlbumBaseFilter>();
|
||||
|
||||
final List<FilterGridItem<AlbumFilter>> newMapEntries = [], pinnedMapEntries = [], unpinnedMapEntries = [];
|
||||
final List<FilterGridItem<AlbumBaseFilter>> newMapEntries = [], pinnedMapEntries = [], unpinnedMapEntries = [];
|
||||
for (final item in sortedMapEntries) {
|
||||
final filter = item.filter;
|
||||
if (newFilters.contains(filter)) {
|
||||
|
@ -95,24 +102,31 @@ class AlbumListPage extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
var sections = <ChipSectionKey, List<FilterGridItem<AlbumFilter>>>{};
|
||||
var sections = <ChipSectionKey, List<FilterGridItem<AlbumBaseFilter>>>{};
|
||||
switch (settings.albumGroupFactor) {
|
||||
case AlbumChipGroupFactor.importance:
|
||||
final specialKey = AlbumImportanceSectionKey.special(context);
|
||||
final appsKey = AlbumImportanceSectionKey.apps(context);
|
||||
final vaultKey = AlbumImportanceSectionKey.vault(context);
|
||||
final regularKey = AlbumImportanceSectionKey.regular(context);
|
||||
sections = groupBy<FilterGridItem<AlbumFilter>, ChipSectionKey>(unpinnedMapEntries, (kv) {
|
||||
switch (covers.effectiveAlbumType(kv.filter.album)) {
|
||||
case AlbumType.regular:
|
||||
return regularKey;
|
||||
case AlbumType.app:
|
||||
return appsKey;
|
||||
case AlbumType.vault:
|
||||
return vaultKey;
|
||||
default:
|
||||
return specialKey;
|
||||
final dynamicKey = AlbumImportanceSectionKey.dynamic(context);
|
||||
sections = groupBy<FilterGridItem<AlbumBaseFilter>, ChipSectionKey>(unpinnedMapEntries, (kv) {
|
||||
final filter = kv.filter;
|
||||
if (filter is StoredAlbumFilter) {
|
||||
switch (covers.effectiveAlbumType(filter.album)) {
|
||||
case AlbumType.regular:
|
||||
return regularKey;
|
||||
case AlbumType.app:
|
||||
return appsKey;
|
||||
case AlbumType.vault:
|
||||
return vaultKey;
|
||||
default:
|
||||
return specialKey;
|
||||
}
|
||||
} else if (filter is DynamicAlbumFilter) {
|
||||
return dynamicKey;
|
||||
}
|
||||
return specialKey;
|
||||
});
|
||||
|
||||
sections = {
|
||||
|
@ -120,11 +134,12 @@ class AlbumListPage extends StatelessWidget {
|
|||
if (sections.containsKey(specialKey)) specialKey: sections[specialKey]!,
|
||||
if (sections.containsKey(appsKey)) appsKey: sections[appsKey]!,
|
||||
if (sections.containsKey(vaultKey)) vaultKey: sections[vaultKey]!,
|
||||
if (sections.containsKey(dynamicKey)) dynamicKey: sections[dynamicKey]!,
|
||||
if (sections.containsKey(regularKey)) regularKey: sections[regularKey]!,
|
||||
};
|
||||
case AlbumChipGroupFactor.mimeType:
|
||||
final visibleEntries = source.visibleEntries;
|
||||
sections = groupBy<FilterGridItem<AlbumFilter>, ChipSectionKey>(unpinnedMapEntries, (kv) {
|
||||
sections = groupBy<FilterGridItem<AlbumBaseFilter>, ChipSectionKey>(unpinnedMapEntries, (kv) {
|
||||
final matches = visibleEntries.where(kv.filter.test);
|
||||
final hasImage = matches.any((v) => v.isImage);
|
||||
final hasVideo = matches.any((v) => v.isVideo);
|
||||
|
@ -133,8 +148,8 @@ class AlbumListPage extends StatelessWidget {
|
|||
return MimeTypeSectionKey.mixed(context);
|
||||
});
|
||||
case AlbumChipGroupFactor.volume:
|
||||
sections = groupBy<FilterGridItem<AlbumFilter>, ChipSectionKey>(unpinnedMapEntries, (kv) {
|
||||
return StorageVolumeSectionKey(context, androidFileUtils.getStorageVolume(kv.filter.album));
|
||||
sections = groupBy<FilterGridItem<AlbumBaseFilter>, ChipSectionKey>(unpinnedMapEntries, (kv) {
|
||||
return StorageVolumeSectionKey(context, kv.filter.storageVolume);
|
||||
});
|
||||
case AlbumChipGroupFactor.none:
|
||||
return {
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/dynamic_albums.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/dynamic_album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
|
@ -14,6 +17,7 @@ import 'package:aves/services/common/services.dart';
|
|||
import 'package:aves/services/media/enums.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/utils/collection_utils.dart';
|
||||
import 'package:aves/view/view.dart';
|
||||
import 'package:aves/widgets/common/action_mixins/entry_storage.dart';
|
||||
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||||
|
@ -21,9 +25,10 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
|||
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/filter_editors/create_album_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/filter_editors/create_stored_album_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/filter_editors/edit_vault_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/filter_editors/rename_album_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/filter_editors/rename_dynamic_album_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/filter_editors/rename_stored_album_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/tile_view_dialog.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart';
|
||||
|
@ -33,13 +38,13 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with EntryStorageMixin {
|
||||
final Iterable<FilterGridItem<AlbumFilter>> _items;
|
||||
class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumBaseFilter> with EntryStorageMixin {
|
||||
final Iterable<FilterGridItem<AlbumBaseFilter>> _items;
|
||||
|
||||
AlbumChipSetActionDelegate(Iterable<FilterGridItem<AlbumFilter>> items) : _items = items;
|
||||
AlbumChipSetActionDelegate(Iterable<FilterGridItem<AlbumBaseFilter>> items) : _items = items;
|
||||
|
||||
@override
|
||||
Iterable<FilterGridItem<AlbumFilter>> get allItems => _items;
|
||||
Iterable<FilterGridItem<AlbumBaseFilter>> get allItems => _items;
|
||||
|
||||
@override
|
||||
ChipSortFactor get sortFactor => settings.albumSortFactor;
|
||||
|
@ -72,7 +77,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
required AppMode appMode,
|
||||
required bool isSelecting,
|
||||
required int itemCount,
|
||||
required Set<AlbumFilter> selectedFilters,
|
||||
required Set<AlbumBaseFilter> selectedFilters,
|
||||
}) {
|
||||
final selectedSingleItem = selectedFilters.length == 1;
|
||||
final isMain = appMode == AppMode.main;
|
||||
|
@ -82,14 +87,17 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
case ChipSetAction.createVault:
|
||||
return !settings.isReadOnly && appMode.canCreateFilter && !isSelecting;
|
||||
case ChipSetAction.delete:
|
||||
return isMain && isSelecting && !settings.isReadOnly && !(selectedFilters.whereType<StoredAlbumFilter>().isEmpty && selectedFilters.whereType<DynamicAlbumFilter>().isNotEmpty);
|
||||
case ChipSetAction.remove:
|
||||
return isMain && isSelecting && !settings.isReadOnly && selectedFilters.whereType<StoredAlbumFilter>().isEmpty && selectedFilters.whereType<DynamicAlbumFilter>().isNotEmpty;
|
||||
case ChipSetAction.rename:
|
||||
return isMain && isSelecting && !settings.isReadOnly;
|
||||
case ChipSetAction.hide:
|
||||
return isMain && selectedFilters.none((v) => vaults.isVault(v.album));
|
||||
return isMain && selectedFilters.none((v) => v.isVault);
|
||||
case ChipSetAction.configureVault:
|
||||
return isMain && selectedSingleItem && vaults.isVault(selectedFilters.first.album);
|
||||
return isMain && selectedSingleItem && selectedFilters.first.isVault;
|
||||
case ChipSetAction.lockVault:
|
||||
return isMain && selectedFilters.any((v) => vaults.isVault(v.album));
|
||||
return isMain && selectedFilters.any((v) => v.isVault);
|
||||
default:
|
||||
return super.isVisible(
|
||||
action,
|
||||
|
@ -106,25 +114,20 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
ChipSetAction action, {
|
||||
required bool isSelecting,
|
||||
required int itemCount,
|
||||
required Set<AlbumFilter> selectedFilters,
|
||||
required Set<AlbumBaseFilter> selectedFilters,
|
||||
}) {
|
||||
final selectedItemCount = selectedFilters.length;
|
||||
final hasSelection = selectedItemCount > 0;
|
||||
|
||||
switch (action) {
|
||||
case ChipSetAction.delete:
|
||||
return selectedFilters.whereType<StoredAlbumFilter>().isNotEmpty && selectedFilters.whereType<DynamicAlbumFilter>().isEmpty;
|
||||
case ChipSetAction.rename:
|
||||
if (selectedFilters.length != 1) return false;
|
||||
|
||||
final dirPath = selectedFilters.first.album;
|
||||
if (vaults.isVault(dirPath)) return true;
|
||||
|
||||
// do not allow renaming volume root
|
||||
final dir = androidFileUtils.relativeDirectoryFromPath(dirPath);
|
||||
return dir != null && dir.relativeDir.isNotEmpty;
|
||||
return selectedFilters.length == 1 && selectedFilters.first.canRename;
|
||||
case ChipSetAction.hide:
|
||||
return hasSelection;
|
||||
case ChipSetAction.lockVault:
|
||||
return selectedFilters.map((v) => v.album).any((v) => vaults.isVault(v) && !vaults.isLocked(v));
|
||||
return selectedFilters.whereType<StoredAlbumFilter>().map((v) => v.album).any((v) => vaults.isVault(v) && !vaults.isLocked(v));
|
||||
case ChipSetAction.configureVault:
|
||||
return true;
|
||||
default:
|
||||
|
@ -143,14 +146,16 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
switch (action) {
|
||||
// general
|
||||
case ChipSetAction.createAlbum:
|
||||
_createAlbum(context, locked: false);
|
||||
_createStoredAlbum(context, locked: false);
|
||||
case ChipSetAction.createVault:
|
||||
_createAlbum(context, locked: true);
|
||||
_createStoredAlbum(context, locked: true);
|
||||
// single/multiple filters
|
||||
case ChipSetAction.delete:
|
||||
_delete(context);
|
||||
_deleteStoredAlbums(context);
|
||||
case ChipSetAction.remove:
|
||||
_removeDynamicAlbum(context);
|
||||
case ChipSetAction.lockVault:
|
||||
lockFilters(getSelectedFilters(context));
|
||||
lockFilters(_getSelectedStoredAlbumFilters(context));
|
||||
browse(context);
|
||||
// single filter
|
||||
case ChipSetAction.rename:
|
||||
|
@ -163,6 +168,14 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
super.onActionSelected(context, action);
|
||||
}
|
||||
|
||||
Set<StoredAlbumFilter> _getSelectedStoredAlbumFilters(BuildContext context) {
|
||||
return getSelectedFilters(context).whereType<StoredAlbumFilter>().toSet();
|
||||
}
|
||||
|
||||
Set<DynamicAlbumFilter> _getSelectedDynamicAlbumFilters(BuildContext context) {
|
||||
return getSelectedFilters(context).whereType<DynamicAlbumFilter>().toSet();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> configureView(BuildContext context) async {
|
||||
final initialValue = (
|
||||
|
@ -196,7 +209,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
}
|
||||
}
|
||||
|
||||
void _createAlbum(BuildContext context, {required bool locked}) async {
|
||||
void _createStoredAlbum(BuildContext context, {required bool locked}) async {
|
||||
final l10n = context.l10n;
|
||||
final source = context.read<CollectionSource>();
|
||||
|
||||
|
@ -218,7 +231,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
final details = await showDialog<VaultDetails>(
|
||||
context: context,
|
||||
builder: (context) => const EditVaultDialog(),
|
||||
routeSettings: const RouteSettings(name: CreateAlbumDialog.routeName),
|
||||
routeSettings: const RouteSettings(name: CreateStoredAlbumDialog.routeName),
|
||||
);
|
||||
if (details == null) return;
|
||||
|
||||
|
@ -227,15 +240,15 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
} else {
|
||||
directory = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => const CreateAlbumDialog(),
|
||||
routeSettings: const RouteSettings(name: CreateAlbumDialog.routeName),
|
||||
builder: (context) => const CreateStoredAlbumDialog(),
|
||||
routeSettings: const RouteSettings(name: CreateStoredAlbumDialog.routeName),
|
||||
);
|
||||
if (directory == null) return;
|
||||
|
||||
// wait for the dialog to hide
|
||||
await Future.delayed(ADurations.dialogTransitionLoose * timeDilation);
|
||||
}
|
||||
final filter = AlbumFilter(directory, source.getAlbumDisplayName(context, directory));
|
||||
final filter = StoredAlbumFilter(directory, source.getStoredAlbumDisplayName(context, directory));
|
||||
|
||||
final albumExists = source.rawAlbums.contains(directory);
|
||||
if (albumExists) {
|
||||
|
@ -243,7 +256,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
await _showAlbum(navigator, filter);
|
||||
} else {
|
||||
// create the album and mark it as new
|
||||
source.createAlbum(directory);
|
||||
source.createStoredAlbum(directory);
|
||||
|
||||
final showAction = SnackBarAction(
|
||||
label: l10n.showButtonLabel,
|
||||
|
@ -253,7 +266,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _showAlbum(NavigatorState? navigator, AlbumFilter filter) async {
|
||||
Future<void> _showAlbum(NavigatorState? navigator, StoredAlbumFilter filter) async {
|
||||
// local context may be deactivated when action is triggered after navigation
|
||||
if (navigator != null) {
|
||||
final context = navigator.context;
|
||||
|
@ -273,9 +286,9 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _delete(BuildContext context) async {
|
||||
final filters = getSelectedFilters(context);
|
||||
final byBinUsage = groupBy<AlbumFilter, bool>(filters, (filter) {
|
||||
Future<void> _deleteStoredAlbums(BuildContext context) async {
|
||||
final filters = _getSelectedStoredAlbumFilters(context);
|
||||
final byBinUsage = groupBy<StoredAlbumFilter, bool>(filters, (filter) {
|
||||
final details = vaults.getVault(filter.album);
|
||||
return details?.useBin ?? settings.enableBin;
|
||||
});
|
||||
|
@ -291,7 +304,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
|
||||
Future<void> _doDelete({
|
||||
required BuildContext context,
|
||||
required Set<AlbumFilter> filters,
|
||||
required Set<StoredAlbumFilter> filters,
|
||||
required bool enableBin,
|
||||
}) async {
|
||||
if (!await unlockFilters(context, filters)) return;
|
||||
|
@ -388,44 +401,125 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> _removeDynamicAlbum(BuildContext context) async {
|
||||
final l10n = context.l10n;
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AvesDialog(
|
||||
content: Text(l10n.genericDangerWarningDialogMessage),
|
||||
actions: [
|
||||
const CancelButton(),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.maybeOf(context)?.pop(true),
|
||||
child: Text(l10n.applyButtonLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
routeSettings: const RouteSettings(name: AvesDialog.warningRouteName),
|
||||
);
|
||||
if (confirmed == null || !confirmed) return;
|
||||
|
||||
final albumFilters = _getSelectedDynamicAlbumFilters(context);
|
||||
final names = albumFilters.map((v) => v.name).toSet();
|
||||
bool isRemoved(CollectionFilter v) => v is DynamicAlbumFilter && names.contains(v.name);
|
||||
|
||||
await dynamicAlbums.remove(albumFilters);
|
||||
|
||||
// cleanup
|
||||
await covers.removeAll(albumFilters, notify: true);
|
||||
settings.drawerAlbumBookmarks = settings.drawerAlbumBookmarks?..removeWhere(isRemoved);
|
||||
settings.pinnedFilters = settings.pinnedFilters..removeWhere(isRemoved);
|
||||
|
||||
browse(context);
|
||||
}
|
||||
|
||||
Future<void> _rename(BuildContext context) async {
|
||||
final filters = getSelectedFilters(context);
|
||||
if (filters.isEmpty) return;
|
||||
|
||||
final filter = filters.first;
|
||||
if (!await unlockFilter(context, filter)) return;
|
||||
if (filter is StoredAlbumFilter) {
|
||||
if (!await unlockFilter(context, filter)) return;
|
||||
|
||||
final album = filter.album;
|
||||
if (!vaults.isVault(album)) {
|
||||
final dir = androidFileUtils.relativeDirectoryFromPath(album);
|
||||
// do not allow renaming volume root
|
||||
if (dir == null || dir.relativeDir.isEmpty) return;
|
||||
final album = filter.album;
|
||||
if (!vaults.isVault(album)) {
|
||||
final dir = androidFileUtils.relativeDirectoryFromPath(album);
|
||||
// do not allow renaming volume root
|
||||
if (dir == null || dir.relativeDir.isEmpty) return;
|
||||
|
||||
// check whether renaming is possible given OS restrictions,
|
||||
// before asking to input a new name
|
||||
final restrictedDirsLowerCase = await storageService.getRestrictedDirectoriesLowerCase();
|
||||
if (restrictedDirsLowerCase.contains(dir.copyWith(relativeDir: dir.relativeDir.toLowerCase()))) {
|
||||
await showRestrictedDirectoryDialog(context, dir);
|
||||
return;
|
||||
// check whether renaming is possible given OS restrictions,
|
||||
// before asking to input a new name
|
||||
final restrictedDirsLowerCase = await storageService.getRestrictedDirectoriesLowerCase();
|
||||
if (restrictedDirsLowerCase.contains(dir.copyWith(relativeDir: dir.relativeDir.toLowerCase()))) {
|
||||
await showRestrictedDirectoryDialog(context, dir);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final newName = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => RenameStoredAlbumDialog(album: album),
|
||||
routeSettings: const RouteSettings(name: RenameStoredAlbumDialog.routeName),
|
||||
);
|
||||
if (newName == null || newName.isEmpty) return;
|
||||
|
||||
await _doRenameStoredAlbum(context, filter, newName);
|
||||
} else if (filter is DynamicAlbumFilter) {
|
||||
final newName = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => RenameDynamicAlbumDialog(name: filter.name),
|
||||
routeSettings: const RouteSettings(name: RenameStoredAlbumDialog.routeName),
|
||||
);
|
||||
if (newName == null || newName.isEmpty) return;
|
||||
|
||||
await _doRenameDynamicAlbum(context, filter, newName);
|
||||
}
|
||||
|
||||
final newName = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => RenameAlbumDialog(album: album),
|
||||
routeSettings: const RouteSettings(name: RenameAlbumDialog.routeName),
|
||||
);
|
||||
if (newName == null || newName.isEmpty) return;
|
||||
|
||||
await _doRename(context, filter, newName);
|
||||
}
|
||||
|
||||
Future<void> _doRename(BuildContext context, AlbumFilter filter, String newName) async {
|
||||
Future<void> _doRenameDynamicAlbum(BuildContext context, DynamicAlbumFilter albumFilter, String newName) async {
|
||||
final oldName = albumFilter.name;
|
||||
|
||||
// save cover and bookmark before renaming
|
||||
final cover = await covers.remove(albumFilter, notify: false);
|
||||
final bookmarks = settings.drawerAlbumBookmarks;
|
||||
final pinnedFilters = settings.pinnedFilters;
|
||||
|
||||
await dynamicAlbums.rename(albumFilter, newName);
|
||||
final newFilter = DynamicAlbumFilter(newName, albumFilter.filter);
|
||||
bool isRenamed(CollectionFilter v) => v is DynamicAlbumFilter && v.name == oldName;
|
||||
|
||||
// update cover
|
||||
if (cover != null) {
|
||||
await covers.set(
|
||||
filter: newFilter,
|
||||
entryId: cover.$1,
|
||||
packageName: cover.$2,
|
||||
color: cover.$3,
|
||||
notify: true,
|
||||
);
|
||||
}
|
||||
// update drawer bookmark
|
||||
final bookmark = bookmarks?.firstWhereOrNull(isRenamed);
|
||||
if (bookmark != null) {
|
||||
bookmarks?.replace(bookmark, newFilter);
|
||||
settings.drawerAlbumBookmarks = bookmarks;
|
||||
}
|
||||
// update pin
|
||||
final pin = pinnedFilters.firstWhereOrNull(isRenamed);
|
||||
if (pin != null) {
|
||||
pinnedFilters.replace(pin, newFilter);
|
||||
settings.pinnedFilters = pinnedFilters;
|
||||
}
|
||||
|
||||
browse(context);
|
||||
}
|
||||
|
||||
Future<void> _doRenameStoredAlbum(BuildContext context, StoredAlbumFilter albumFilter, String newName) async {
|
||||
final l10n = context.l10n;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final source = context.read<CollectionSource>();
|
||||
final album = filter.album;
|
||||
final todoEntries = source.visibleEntries.where(filter.test).toSet();
|
||||
final album = albumFilter.album;
|
||||
final todoEntries = source.visibleEntries.where(albumFilter.test).toSet();
|
||||
final todoCount = todoEntries.length;
|
||||
|
||||
final destinationAlbumParent = pContext.dirname(album);
|
||||
|
@ -455,7 +549,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
onDone: (processed) async {
|
||||
final successOps = processed.where((e) => e.success).toSet();
|
||||
final movedOps = successOps.where((e) => !e.skipped).toSet();
|
||||
await source.renameAlbum(album, destinationAlbum, todoEntries, movedOps);
|
||||
await source.renameStoredAlbum(album, destinationAlbum, todoEntries, movedOps);
|
||||
browse(context);
|
||||
source.resumeMonitoring();
|
||||
|
||||
|
@ -478,6 +572,8 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
if (filters.isEmpty) return;
|
||||
|
||||
final filter = filters.first;
|
||||
if (filter is! StoredAlbumFilter) return;
|
||||
|
||||
if (!await unlockFilter(context, filter)) return;
|
||||
|
||||
final oldDetails = vaults.getVault(filter.album);
|
||||
|
@ -491,7 +587,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
if (newDetails == null || oldDetails == newDetails) return;
|
||||
|
||||
if (oldDetails.useBin && !newDetails.useBin) {
|
||||
final filter = AlbumFilter(oldDetails.path, null);
|
||||
final filter = StoredAlbumFilter(oldDetails.path, null);
|
||||
final source = context.read<CollectionSource>();
|
||||
await _deleteEntriesForever(context, source.trashedEntries.where(filter.test).toSet());
|
||||
}
|
||||
|
@ -503,7 +599,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
// wipe the old pass, if any, so that it does not overwrite the new pass
|
||||
// when renaming the vault afterwards
|
||||
await securityService.writeValue(oldDetails.passKey, null);
|
||||
await _doRename(context, filter, newName);
|
||||
await _doRenameStoredAlbum(context, filter, newName);
|
||||
} else {
|
||||
await vaults.update(newDetails);
|
||||
browse(context);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/path.dart';
|
||||
import 'package:aves/model/filters/rating.dart';
|
||||
|
@ -35,9 +35,9 @@ class ChipActionDelegate with FeedbackMixin, VaultAwareMixin {
|
|||
case ChipAction.reverse:
|
||||
return true;
|
||||
case ChipAction.hide:
|
||||
return !(filter is AlbumFilter && vaults.isVault(filter.album));
|
||||
return !(filter is StoredAlbumFilter && vaults.isVault(filter.album));
|
||||
case ChipAction.lockVault:
|
||||
return (filter is AlbumFilter && vaults.isVault(filter.album) && !vaults.isLocked(filter.album));
|
||||
return (filter is StoredAlbumFilter && vaults.isVault(filter.album) && !vaults.isLocked(filter.album));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ class ChipActionDelegate with FeedbackMixin, VaultAwareMixin {
|
|||
_goTo(context, filter, TagListPage.routeName, (context) => const TagListPage());
|
||||
case ChipAction.goToExplorerPage:
|
||||
String? path;
|
||||
if (filter is AlbumFilter) {
|
||||
if (filter is StoredAlbumFilter) {
|
||||
path = filter.album;
|
||||
} else if (filter is PathFilter) {
|
||||
path = filter.path;
|
||||
|
@ -77,7 +77,7 @@ class ChipActionDelegate with FeedbackMixin, VaultAwareMixin {
|
|||
case ChipAction.hide:
|
||||
_hide(context, filter);
|
||||
case ChipAction.lockVault:
|
||||
if (filter is AlbumFilter) {
|
||||
if (filter is StoredAlbumFilter) {
|
||||
lockFilters({filter});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/or.dart';
|
||||
import 'package:aves/model/filters/set_or.dart';
|
||||
import 'package:aves/model/query.dart';
|
||||
import 'package:aves/model/selection.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
|
@ -107,6 +107,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
|
|||
case ChipSetAction.showCollection:
|
||||
return appMode.canNavigate;
|
||||
case ChipSetAction.delete:
|
||||
case ChipSetAction.remove:
|
||||
case ChipSetAction.lockVault:
|
||||
case ChipSetAction.showCountryStates:
|
||||
return false;
|
||||
|
@ -148,6 +149,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
|
|||
return (!isSelecting && hasItems) || (isSelecting && hasSelection);
|
||||
// selecting (single/multiple filters)
|
||||
case ChipSetAction.delete:
|
||||
case ChipSetAction.remove:
|
||||
case ChipSetAction.hide:
|
||||
case ChipSetAction.pin:
|
||||
case ChipSetAction.unpin:
|
||||
|
@ -204,6 +206,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
|
|||
case ChipSetAction.showCollection:
|
||||
_goToCollection(context);
|
||||
case ChipSetAction.delete:
|
||||
case ChipSetAction.remove:
|
||||
case ChipSetAction.lockVault:
|
||||
case ChipSetAction.showCountryStates:
|
||||
break;
|
||||
|
@ -264,7 +267,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
|
|||
final filters = getSelectedFilters(context);
|
||||
if (filters.isEmpty) return;
|
||||
|
||||
final filter = filters.length > 1 ? OrFilter(filters) : filters.first;
|
||||
final filter = filters.length > 1 ? SetOrFilter(filters) : filters.first;
|
||||
await Navigator.maybeOf(context)?.push(
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: CollectionPage.routeName),
|
||||
|
@ -378,7 +381,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
|
|||
);
|
||||
if (selectedCover == null) return;
|
||||
|
||||
if (filter is AlbumFilter) {
|
||||
if (filter is StoredAlbumFilter) {
|
||||
context.read<AvesColorsData>().clearAppColor(filter.album);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/geo/states.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart';
|
||||
import 'package:aves/widgets/filter_grids/places_page.dart';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart';
|
||||
import 'package:aves/widgets/filter_grids/states_page.dart';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/filters/covered/tag.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
|
@ -50,7 +50,7 @@ class TagChipSetActionDelegate extends ChipSetActionDelegate<TagFilter> {
|
|||
final isMain = appMode == AppMode.main;
|
||||
|
||||
switch (action) {
|
||||
case ChipSetAction.delete:
|
||||
case ChipSetAction.remove:
|
||||
return isMain && isSelecting && !settings.isReadOnly;
|
||||
default:
|
||||
return super.isVisible(
|
||||
|
@ -68,30 +68,31 @@ class TagChipSetActionDelegate extends ChipSetActionDelegate<TagFilter> {
|
|||
reportService.log('$runtimeType handles $action');
|
||||
switch (action) {
|
||||
// single/multiple filters
|
||||
case ChipSetAction.delete:
|
||||
_delete(context);
|
||||
case ChipSetAction.remove:
|
||||
_remove(context);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
super.onActionSelected(context, action);
|
||||
}
|
||||
|
||||
Future<void> _delete(BuildContext context) async {
|
||||
Future<void> _remove(BuildContext context) async {
|
||||
final filters = getSelectedFilters(context);
|
||||
|
||||
final source = context.read<CollectionSource>();
|
||||
final todoEntries = source.visibleEntries.where((entry) => filters.any((f) => f.test(entry))).toSet();
|
||||
final todoTags = filters.map((v) => v.tag).toSet();
|
||||
|
||||
final l10n = context.l10n;
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AvesDialog(
|
||||
content: Text(context.l10n.genericDangerWarningDialogMessage),
|
||||
content: Text(l10n.genericDangerWarningDialogMessage),
|
||||
actions: [
|
||||
const CancelButton(),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.maybeOf(context)?.pop(true),
|
||||
child: Text(context.l10n.applyButtonLabel),
|
||||
child: Text(l10n.applyButtonLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -2,10 +2,11 @@ import 'dart:math';
|
|||
|
||||
import 'package:aves/model/app_inventory.dart';
|
||||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/dynamic_album.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/covered/tag.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/source/album.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/location/country.dart';
|
||||
|
@ -69,11 +70,18 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
|
|||
builder: (context, snapshot) => Consumer<CollectionSource>(
|
||||
builder: (context, source, child) {
|
||||
switch (filter) {
|
||||
case AlbumFilter filter:
|
||||
case StoredAlbumFilter filter:
|
||||
{
|
||||
final album = filter.album;
|
||||
return StreamBuilder<AlbumSummaryInvalidatedEvent>(
|
||||
stream: source.eventBus.on<AlbumSummaryInvalidatedEvent>().where((event) => event.directories == null || event.directories!.contains(album)),
|
||||
return StreamBuilder<StoredAlbumSummaryInvalidatedEvent>(
|
||||
stream: source.eventBus.on<StoredAlbumSummaryInvalidatedEvent>().where((event) => event.directories == null || event.directories!.contains(album)),
|
||||
builder: (context, snapshot) => _buildChip(context, source),
|
||||
);
|
||||
}
|
||||
case DynamicAlbumFilter _:
|
||||
{
|
||||
return StreamBuilder<DynamicAlbumSummaryInvalidatedEvent>(
|
||||
stream: source.eventBus.on<DynamicAlbumSummaryInvalidatedEvent>(),
|
||||
builder: (context, snapshot) => _buildChip(context, source),
|
||||
);
|
||||
}
|
||||
|
@ -103,10 +111,10 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
|
|||
|
||||
Widget _buildChip(BuildContext context, CollectionSource source) {
|
||||
final _filter = filter;
|
||||
final entry = _filter is AlbumFilter && vaults.isLocked(_filter.album) ? null : source.coverEntry(_filter);
|
||||
final entry = _filter is StoredAlbumFilter && vaults.isLocked(_filter.album) ? null : source.coverEntry(_filter);
|
||||
final titlePadding = min<double>(4.0, extent / 32);
|
||||
Key? chipKey;
|
||||
if (_filter is AlbumFilter) {
|
||||
if (_filter is StoredAlbumFilter) {
|
||||
// when we asynchronously fetch installed app names,
|
||||
// album filters themselves do not change, but decoration derived from it does
|
||||
chipKey = ValueKey(appInventory.areAppNamesReadyNotifier.value);
|
||||
|
@ -172,52 +180,35 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
|
|||
Color _detailColor(BuildContext context) => Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
|
||||
Widget _buildDetails(BuildContext context, CollectionSource source, T filter) {
|
||||
final countFormatter = NumberFormat.decimalPattern(context.locale);
|
||||
|
||||
final padding = min<double>(8.0, extent / 16);
|
||||
final iconSize = detailIconSize(extent);
|
||||
final fontSize = detailFontSize(extent);
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (pinned)
|
||||
AnimatedPadding(
|
||||
padding: EdgeInsetsDirectional.only(end: padding),
|
||||
duration: ADurations.chipDecorationAnimation,
|
||||
child: Icon(
|
||||
AIcons.pin,
|
||||
color: _detailColor(context),
|
||||
size: iconSize,
|
||||
),
|
||||
),
|
||||
if (filter is AlbumFilter && androidFileUtils.isOnRemovableStorage(filter.album))
|
||||
AnimatedPadding(
|
||||
padding: EdgeInsetsDirectional.only(end: padding),
|
||||
duration: ADurations.chipDecorationAnimation,
|
||||
child: Icon(
|
||||
AIcons.storageCard,
|
||||
color: _detailColor(context),
|
||||
size: iconSize,
|
||||
),
|
||||
),
|
||||
if (filter is AlbumFilter && vaults.isVault(filter.album))
|
||||
AnimatedPadding(
|
||||
padding: EdgeInsetsDirectional.only(end: padding),
|
||||
duration: ADurations.chipDecorationAnimation,
|
||||
child: Icon(
|
||||
AIcons.locked,
|
||||
color: _detailColor(context),
|
||||
size: iconSize,
|
||||
),
|
||||
),
|
||||
if (pinned) _buildDetailIcon(context, AIcons.pin),
|
||||
if (filter is StoredAlbumFilter && androidFileUtils.isOnRemovableStorage(filter.album)) _buildDetailIcon(context, AIcons.storageCard),
|
||||
if (filter is StoredAlbumFilter && vaults.isVault(filter.album)) _buildDetailIcon(context, AIcons.locked),
|
||||
if (filter is DynamicAlbumFilter) _buildDetailIcon(context, AIcons.dynamicAlbum),
|
||||
Text(
|
||||
locked ? AText.valueNotAvailable : countFormatter.format(source.count(filter)),
|
||||
locked ? AText.valueNotAvailable : NumberFormat.decimalPattern(context.locale).format(source.count(filter)),
|
||||
style: TextStyle(
|
||||
color: _detailColor(context),
|
||||
fontSize: fontSize,
|
||||
fontSize: detailFontSize(extent),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailIcon(BuildContext context, IconData icon) {
|
||||
final padding = min<double>(8.0, extent / 16);
|
||||
final iconSize = detailIconSize(extent);
|
||||
return AnimatedPadding(
|
||||
padding: EdgeInsetsDirectional.only(end: padding),
|
||||
duration: ADurations.chipDecorationAnimation,
|
||||
child: Icon(
|
||||
icon,
|
||||
color: _detailColor(context),
|
||||
size: iconSize,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'package:aves/theme/icons.dart';
|
|||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum AlbumImportance { newAlbum, pinned, special, apps, vaults, regular }
|
||||
enum AlbumImportance { newAlbum, pinned, special, apps, vaults, dynamic, regular }
|
||||
|
||||
extension ExtraAlbumImportance on AlbumImportance {
|
||||
String getText(BuildContext context) {
|
||||
|
@ -13,6 +13,7 @@ extension ExtraAlbumImportance on AlbumImportance {
|
|||
AlbumImportance.special => l10n.albumTierSpecial,
|
||||
AlbumImportance.apps => l10n.albumTierApps,
|
||||
AlbumImportance.vaults => l10n.albumTierVaults,
|
||||
AlbumImportance.dynamic => l10n.albumTierDynamic,
|
||||
AlbumImportance.regular => l10n.albumTierRegular,
|
||||
};
|
||||
}
|
||||
|
@ -24,6 +25,7 @@ extension ExtraAlbumImportance on AlbumImportance {
|
|||
AlbumImportance.special => AIcons.important,
|
||||
AlbumImportance.apps => AIcons.app,
|
||||
AlbumImportance.vaults => AIcons.locked,
|
||||
AlbumImportance.dynamic => AIcons.dynamicAlbum,
|
||||
AlbumImportance.regular => AIcons.album,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/selection.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
|
@ -134,7 +134,7 @@ class FilterTile<T extends CollectionFilter> extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
final filter = gridItem.filter;
|
||||
final pinned = settings.pinnedFilters.contains(filter);
|
||||
final locked = filter is AlbumFilter && vaults.isLocked(filter.album);
|
||||
final locked = filter is StoredAlbumFilter && vaults.isLocked(filter.album);
|
||||
final onChipTap = onTap != null ? (filter) => onTap?.call() : null;
|
||||
|
||||
switch (tileLayout) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/dynamic_album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/theme/format.dart';
|
||||
|
@ -116,11 +117,12 @@ class FilterListDetails<T extends CollectionFilter> extends StatelessWidget {
|
|||
|
||||
Widget _buildCountRow(BuildContext context, FilterListDetailsThemeData detailsTheme, bool hasTitleLeading) {
|
||||
final _filter = filter;
|
||||
final removableStorage = _filter is AlbumFilter && androidFileUtils.isOnRemovableStorage(_filter.album);
|
||||
final removableStorage = _filter is StoredAlbumFilter && androidFileUtils.isOnRemovableStorage(_filter.album);
|
||||
|
||||
List<Widget> leadingIcons = [
|
||||
if (pinned) const Icon(AIcons.pin),
|
||||
if (removableStorage) const Icon(AIcons.storageCard),
|
||||
if (_filter is DynamicAlbumFilter) const Icon(AIcons.dynamicAlbum),
|
||||
];
|
||||
|
||||
Widget? leading;
|
||||
|
|
|
@ -35,6 +35,8 @@ class AlbumImportanceSectionKey extends ChipSectionKey {
|
|||
|
||||
factory AlbumImportanceSectionKey.vault(BuildContext context) => AlbumImportanceSectionKey._private(context, AlbumImportance.vaults);
|
||||
|
||||
factory AlbumImportanceSectionKey.dynamic(BuildContext context) => AlbumImportanceSectionKey._private(context, AlbumImportance.dynamic);
|
||||
|
||||
factory AlbumImportanceSectionKey.regular(BuildContext context) => AlbumImportanceSectionKey._private(context, AlbumImportance.regular);
|
||||
|
||||
@override
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/location/country.dart';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/location/place.dart';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:aves/geo/states.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/location/place.dart';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/filters/covered/tag.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/tag.dart';
|
||||
|
|
|
@ -7,9 +7,9 @@ import 'package:aves/model/app/permissions.dart';
|
|||
import 'package:aves/model/app_inventory.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/entry/extensions/catalog.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/settings/enums/home_page.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
|
@ -241,7 +241,7 @@ class _HomePageState extends State<HomePage> {
|
|||
await reportService.log('Initialize source to view item in directory $directory');
|
||||
final source = context.read<CollectionSource>();
|
||||
source.canAnalyze = false;
|
||||
await source.init(scope: {AlbumFilter(directory, null)});
|
||||
await source.init(scope: {StoredAlbumFilter(directory, null)});
|
||||
}
|
||||
} else {
|
||||
await _initViewerEssentials();
|
||||
|
@ -324,7 +324,7 @@ class _HomePageState extends State<HomePage> {
|
|||
|
||||
collection = CollectionLens(
|
||||
source: source,
|
||||
filters: {AlbumFilter(album, source.getAlbumDisplayName(context, album))},
|
||||
filters: {StoredAlbumFilter(album, source.getStoredAlbumDisplayName(context, album))},
|
||||
listenToSource: false,
|
||||
// if we group bursts, opening a burst sub-entry should:
|
||||
// - identify and select the containing main entry,
|
||||
|
|
|
@ -5,7 +5,7 @@ import 'package:aves/model/entry/entry.dart';
|
|||
import 'package:aves/model/entry/extensions/location.dart';
|
||||
import 'package:aves/model/filters/coordinate.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/model/media/geotiff.dart';
|
||||
import 'package:aves/model/settings/enums/accessibility_animations.dart';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/dynamic_album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/trash.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/album.dart';
|
||||
|
@ -46,14 +47,29 @@ class AppDrawer extends StatefulWidget {
|
|||
@override
|
||||
State<AppDrawer> createState() => _AppDrawerState();
|
||||
|
||||
static List<String> getDefaultAlbums(BuildContext context) {
|
||||
static List<AlbumBaseFilter> _getDefaultAlbums(BuildContext context) {
|
||||
final source = context.read<CollectionSource>();
|
||||
final specialAlbums = source.rawAlbums.where((album) {
|
||||
final type = androidFileUtils.getAlbumType(album);
|
||||
return [AlbumType.camera, AlbumType.download, AlbumType.screenshots].contains(type);
|
||||
}).toList()
|
||||
..sort(source.compareAlbumsByName);
|
||||
return specialAlbums;
|
||||
return specialAlbums.map((v) => StoredAlbumFilter(v, source.getStoredAlbumDisplayName(context, v))).toList();
|
||||
}
|
||||
|
||||
static List<AlbumBaseFilter>? _getCustomAlbums(BuildContext context) {
|
||||
final source = context.read<CollectionSource>();
|
||||
return settings.drawerAlbumBookmarks?.map((v) {
|
||||
if (v is StoredAlbumFilter) {
|
||||
final album = v.album;
|
||||
return StoredAlbumFilter(album, source.getStoredAlbumDisplayName(context, album));
|
||||
}
|
||||
return v;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
static List<AlbumBaseFilter> effectiveAlbumBookmarks(BuildContext context) {
|
||||
return _getCustomAlbums(context) ?? _getDefaultAlbums(context);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -288,17 +304,22 @@ class _AppDrawerState extends State<AppDrawer> with WidgetsBindingObserver {
|
|||
return StreamBuilder(
|
||||
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
||||
builder: (context, snapshot) {
|
||||
final albums = settings.drawerAlbumBookmarks ?? AppDrawer.getDefaultAlbums(context);
|
||||
final albums = AppDrawer.effectiveAlbumBookmarks(context);
|
||||
if (albums.isEmpty) return const SizedBox();
|
||||
return Column(
|
||||
children: [
|
||||
const Divider(),
|
||||
...albums.map((album) => AlbumNavTile(
|
||||
album: album,
|
||||
...albums.map((filter) => AlbumNavTile(
|
||||
filter: filter,
|
||||
isSelected: () {
|
||||
if (currentFilters == null || currentFilters.length > 1) return false;
|
||||
final currentFilter = currentFilters.firstOrNull;
|
||||
return currentFilter is AlbumFilter && currentFilter.album == album;
|
||||
if (currentFilter is StoredAlbumFilter && filter is StoredAlbumFilter) {
|
||||
return currentFilter.album == filter.album;
|
||||
} else if (currentFilter is DynamicAlbumFilter && filter is DynamicAlbumFilter) {
|
||||
return currentFilter.name == filter.name;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
)),
|
||||
],
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/navigation/drawer/tile.dart';
|
||||
|
@ -69,23 +68,21 @@ class CollectionNavTile extends StatelessWidget {
|
|||
}
|
||||
|
||||
class AlbumNavTile extends StatelessWidget {
|
||||
final String album;
|
||||
final AlbumBaseFilter filter;
|
||||
final bool Function() isSelected;
|
||||
|
||||
const AlbumNavTile({
|
||||
super.key,
|
||||
required this.album,
|
||||
required this.filter,
|
||||
required this.isSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final source = context.read<CollectionSource>();
|
||||
final filter = AlbumFilter(album, source.getAlbumDisplayName(context, album));
|
||||
return CollectionNavTile(
|
||||
leading: DrawerFilterIcon(filter: filter),
|
||||
title: DrawerFilterTitle(filter: filter),
|
||||
trailing: androidFileUtils.isOnRemovableStorage(album)
|
||||
trailing: filter.storageVolume?.isRemovable ?? false
|
||||
? const Icon(
|
||||
AIcons.storageCard,
|
||||
size: 16,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/dynamic_album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/settings/enums/home_page.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
|
@ -218,15 +219,18 @@ class _TvRailState extends State<TvRail> {
|
|||
}
|
||||
|
||||
List<_NavEntry> _buildAlbumLinks(BuildContext context) {
|
||||
final source = context.read<CollectionSource>();
|
||||
final currentFilters = currentCollection?.filters;
|
||||
final albums = settings.drawerAlbumBookmarks ?? AppDrawer.getDefaultAlbums(context);
|
||||
return albums.map((album) {
|
||||
final filter = AlbumFilter(album, source.getAlbumDisplayName(context, album));
|
||||
final albums = AppDrawer.effectiveAlbumBookmarks(context);
|
||||
return albums.map((filter) {
|
||||
bool isSelected() {
|
||||
if (currentFilters == null || currentFilters.length > 1) return false;
|
||||
final currentFilter = currentFilters.firstOrNull;
|
||||
return currentFilter is AlbumFilter && currentFilter.album == album;
|
||||
if (currentFilter is StoredAlbumFilter && filter is StoredAlbumFilter) {
|
||||
return currentFilter.album == filter.album;
|
||||
} else if (currentFilter is DynamicAlbumFilter && filter is DynamicAlbumFilter) {
|
||||
return currentFilter.name == filter.name;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return _NavEntry(
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/dynamic_albums.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/aspect_ratio.dart';
|
||||
import 'package:aves/model/filters/date.dart';
|
||||
import 'package:aves/model/filters/favourite.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/filters/mime.dart';
|
||||
import 'package:aves/model/filters/missing.dart';
|
||||
import 'package:aves/model/filters/query.dart';
|
||||
import 'package:aves/model/filters/rating.dart';
|
||||
import 'package:aves/model/filters/recent.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/filters/covered/tag.dart';
|
||||
import 'package:aves/model/filters/type.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/album.dart';
|
||||
|
@ -192,23 +193,27 @@ class CollectionSearchDelegate extends AvesSearchDelegate with FeedbackMixin, Va
|
|||
}
|
||||
|
||||
Widget _buildAlbumFilters(_ContainQuery containQuery) {
|
||||
return StreamBuilder(
|
||||
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
||||
builder: (context, snapshot) {
|
||||
final filters = source.rawAlbums
|
||||
.map((album) => AlbumFilter(
|
||||
album,
|
||||
source.getAlbumDisplayName(context, album),
|
||||
))
|
||||
.where((filter) => containQuery(filter.displayName ?? filter.album))
|
||||
.toList()
|
||||
..sort();
|
||||
return _buildFilterRow(
|
||||
context: context,
|
||||
title: context.l10n.searchAlbumsSectionTitle,
|
||||
filters: filters,
|
||||
);
|
||||
},
|
||||
return AnimatedBuilder(
|
||||
animation: dynamicAlbums,
|
||||
builder: (context, child) => StreamBuilder(
|
||||
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
||||
builder: (context, snapshot) {
|
||||
final filters = <AlbumBaseFilter>[
|
||||
...source.rawAlbums
|
||||
.map((album) => StoredAlbumFilter(
|
||||
album,
|
||||
source.getStoredAlbumDisplayName(context, album),
|
||||
))
|
||||
.where((filter) => containQuery(filter.displayName ?? filter.album)),
|
||||
...dynamicAlbums.all,
|
||||
]..sort();
|
||||
return _buildFilterRow(
|
||||
context: context,
|
||||
title: context.l10n.searchAlbumsSectionTitle,
|
||||
filters: filters,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/dynamic_albums.dart';
|
||||
import 'package:aves/model/favourites.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
enum AppExportItem { covers, favourites, settings }
|
||||
enum AppExportItem { covers, dynamicAlbums, favourites, settings }
|
||||
|
||||
extension ExtraAppExportItem on AppExportItem {
|
||||
String getText(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return switch (this) {
|
||||
AppExportItem.covers => l10n.appExportCovers,
|
||||
AppExportItem.dynamicAlbums => l10n.appExportDynamicAlbums,
|
||||
AppExportItem.favourites => l10n.appExportFavourites,
|
||||
AppExportItem.settings => l10n.appExportSettings,
|
||||
};
|
||||
|
@ -20,6 +22,7 @@ extension ExtraAppExportItem on AppExportItem {
|
|||
dynamic export(CollectionSource source) {
|
||||
return switch (this) {
|
||||
AppExportItem.covers => covers.export(source),
|
||||
AppExportItem.dynamicAlbums => dynamicAlbums.export(),
|
||||
AppExportItem.favourites => favourites.export(source),
|
||||
AppExportItem.settings => settings.export(),
|
||||
};
|
||||
|
@ -29,6 +32,8 @@ extension ExtraAppExportItem on AppExportItem {
|
|||
switch (this) {
|
||||
case AppExportItem.covers:
|
||||
covers.import(jsonMap, source);
|
||||
case AppExportItem.dynamicAlbums:
|
||||
dynamicAlbums.import(jsonMap);
|
||||
case AppExportItem.favourites:
|
||||
favourites.import(jsonMap, source);
|
||||
case AppExportItem.settings:
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/recent.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
|
@ -28,7 +29,7 @@ class NavigationDrawerEditorPage extends StatefulWidget {
|
|||
class _NavigationDrawerEditorPageState extends State<NavigationDrawerEditorPage> {
|
||||
final List<CollectionFilter?> _typeItems = [];
|
||||
final Set<CollectionFilter?> _visibleTypes = {};
|
||||
final List<String> _albumItems = [];
|
||||
final List<AlbumBaseFilter> _albumItems = [];
|
||||
final List<String> _pageItems = [];
|
||||
final Set<String> _visiblePages = {};
|
||||
|
||||
|
@ -54,7 +55,7 @@ class _NavigationDrawerEditorPageState extends State<NavigationDrawerEditorPage>
|
|||
_typeItems.addAll(userTypeLinks);
|
||||
_typeItems.addAll(_typeOptions.where((v) => !userTypeLinks.contains(v)));
|
||||
|
||||
_albumItems.addAll(settings.drawerAlbumBookmarks ?? AppDrawer.getDefaultAlbums(context));
|
||||
_albumItems.addAll(AppDrawer.effectiveAlbumBookmarks(context));
|
||||
|
||||
final userPageLinks = settings.drawerPageBookmarks;
|
||||
_visiblePages.addAll(userPageLinks);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/buttons/outlined_button.dart';
|
||||
|
@ -8,10 +7,9 @@ import 'package:aves/widgets/dialogs/pick_dialogs/album_pick_page.dart';
|
|||
import 'package:aves/widgets/navigation/drawer/tile.dart';
|
||||
import 'package:aves/widgets/settings/navigation/drawer_editor_banner.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class DrawerAlbumTab extends StatefulWidget {
|
||||
final List<String> items;
|
||||
final List<AlbumBaseFilter> items;
|
||||
|
||||
const DrawerAlbumTab({
|
||||
super.key,
|
||||
|
@ -23,11 +21,10 @@ class DrawerAlbumTab extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _DrawerAlbumTabState extends State<DrawerAlbumTab> {
|
||||
List<String> get items => widget.items;
|
||||
List<AlbumBaseFilter> get items => widget.items;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final source = context.read<CollectionSource>();
|
||||
return Column(
|
||||
children: [
|
||||
if (!settings.useTvLayout) ...[
|
||||
|
@ -37,11 +34,10 @@ class _DrawerAlbumTabState extends State<DrawerAlbumTab> {
|
|||
Flexible(
|
||||
child: ReorderableListView.builder(
|
||||
itemBuilder: (context, index) {
|
||||
final album = items[index];
|
||||
final filter = AlbumFilter(album, source.getAlbumDisplayName(context, album));
|
||||
void onPressed() => setState(() => items.remove(album));
|
||||
final filter = items[index];
|
||||
void onPressed() => setState(() => items.remove(filter));
|
||||
return ListTile(
|
||||
key: ValueKey(album),
|
||||
key: ValueKey(filter.key),
|
||||
leading: DrawerFilterIcon(filter: filter),
|
||||
title: DrawerFilterTitle(filter: filter),
|
||||
trailing: IconButton(
|
||||
|
@ -68,9 +64,9 @@ class _DrawerAlbumTabState extends State<DrawerAlbumTab> {
|
|||
icon: const Icon(AIcons.add),
|
||||
label: context.l10n.settingsNavigationDrawerAddAlbum,
|
||||
onPressed: () async {
|
||||
final album = await pickAlbum(context: context, moveType: null);
|
||||
if (album == null || items.contains(album)) return;
|
||||
setState(() => items.add(album));
|
||||
final albumFilter = await pickAlbum(context: context, moveType: null, storedAlbumsOnly: false);
|
||||
if (albumFilter == null || items.contains(albumFilter)) return;
|
||||
setState(() => items.add(albumFilter));
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
|
@ -1,224 +0,0 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:aves/model/settings/enums/accessibility_animations.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/view/view.dart';
|
||||
import 'package:aves/widgets/common/basic/font_size_icon_theme.dart';
|
||||
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
||||
import 'package:aves/widgets/common/basic/scaffold.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/buttons/outlined_button.dart';
|
||||
import 'package:aves/widgets/common/identity/empty.dart';
|
||||
import 'package:aves/widgets/settings/privacy/file_picker/crumb_line.dart';
|
||||
import 'package:aves_model/aves_model.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class FilePickerPage extends StatefulWidget {
|
||||
static const routeName = '/file_picker';
|
||||
|
||||
const FilePickerPage({super.key});
|
||||
|
||||
@override
|
||||
State<FilePickerPage> createState() => _FilePickerPageState();
|
||||
}
|
||||
|
||||
class _FilePickerPageState extends State<FilePickerPage> {
|
||||
late VolumeRelativeDirectory _directory;
|
||||
List<Directory>? _contents;
|
||||
|
||||
Set<StorageVolume> get volumes => androidFileUtils.storageVolumes;
|
||||
|
||||
String get currentDirectoryPath => pContext.join(_directory.volumePath, _directory.relativeDir);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final primaryVolume = volumes.firstWhereOrNull((v) => v.isPrimary);
|
||||
if (primaryVolume != null) {
|
||||
_goTo(primaryVolume.path);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final showHidden = settings.filePickerShowHiddenFiles;
|
||||
final visibleContents = _contents?.where((v) {
|
||||
if (showHidden) {
|
||||
return true;
|
||||
} else {
|
||||
final isHidden = pContext.split(v.path).last.startsWith('.');
|
||||
return !isHidden;
|
||||
}
|
||||
}).toList();
|
||||
final animations = context.select<Settings, AccessibilityAnimations>((v) => v.accessibilityAnimations);
|
||||
return PopScope(
|
||||
canPop: _directory.relativeDir.isEmpty,
|
||||
onPopInvokedWithResult: (didPop, result) {
|
||||
if (didPop) return;
|
||||
|
||||
final parent = pContext.dirname(currentDirectoryPath);
|
||||
_goTo(parent);
|
||||
setState(() {});
|
||||
},
|
||||
child: AvesScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_getTitle(context)),
|
||||
actions: [
|
||||
FontSizeIconTheme(
|
||||
child: PopupMenuButton<_PickerAction>(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: _PickerAction.toggleHiddenView,
|
||||
child: MenuRow(text: showHidden ? l10n.filePickerDoNotShowHiddenFiles : l10n.filePickerShowHiddenFiles),
|
||||
),
|
||||
];
|
||||
},
|
||||
onSelected: (action) async {
|
||||
// wait for the popup menu to hide before proceeding with the action
|
||||
await Future.delayed(animations.popUpAnimationDelay * timeDilation);
|
||||
switch (action) {
|
||||
case _PickerAction.toggleHiddenView:
|
||||
settings.filePickerShowHiddenFiles = !showHidden;
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
popUpAnimationStyle: animations.popUpAnimationStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
drawer: _buildDrawer(context),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
_buildCrumbLine(context),
|
||||
const Divider(height: 0),
|
||||
Expanded(
|
||||
child: visibleContents == null
|
||||
? const SizedBox()
|
||||
: visibleContents.isEmpty
|
||||
? Center(
|
||||
child: EmptyContent(
|
||||
icon: AIcons.folder,
|
||||
text: l10n.filePickerNoItems,
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: visibleContents.length,
|
||||
itemBuilder: (context, index) {
|
||||
return index < visibleContents.length ? _buildContentLine(context, visibleContents[index]) : const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
const Divider(height: 0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: AvesOutlinedButton(
|
||||
label: l10n.filePickerUseThisFolder,
|
||||
onPressed: () => Navigator.maybeOf(context)?.pop(currentDirectoryPath),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCrumbLine(BuildContext context) {
|
||||
final crumbStyle = Theme.of(context).textTheme.bodyMedium!;
|
||||
return SizedBox(
|
||||
height: kMinInteractiveDimension,
|
||||
child: DefaultTextStyle(
|
||||
style: crumbStyle.copyWith(
|
||||
color: crumbStyle.color!.withAlpha((255.0 * .4).round()),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
child: CrumbLine(
|
||||
directory: _directory,
|
||||
onTap: (path) {
|
||||
_goTo(path);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getTitle(BuildContext context) {
|
||||
if (_directory.relativeDir.isEmpty) {
|
||||
return _directory.getVolumeDescription(context);
|
||||
}
|
||||
return pContext.split(_directory.relativeDir).last;
|
||||
}
|
||||
|
||||
Widget _buildDrawer(BuildContext context) {
|
||||
return Drawer(
|
||||
child: ListView(
|
||||
children: [
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
context.l10n.filePickerOpenFrom,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
),
|
||||
...volumes.map((v) {
|
||||
final icon = v.isRemovable ? AIcons.storageCard : AIcons.storageMain;
|
||||
return ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text(v.getDescription(context)),
|
||||
onTap: () async {
|
||||
Navigator.maybeOf(context)?.pop();
|
||||
await Future.delayed(ADurations.drawerTransitionLoose);
|
||||
_goTo(v.path);
|
||||
setState(() {});
|
||||
},
|
||||
selected: _directory.volumePath == v.path,
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContentLine(BuildContext context, FileSystemEntity content) {
|
||||
return ListTile(
|
||||
leading: const Icon(AIcons.folder),
|
||||
title: Text('${Unicode.FSI}${pContext.split(content.path).last}${Unicode.PDI}'),
|
||||
onTap: () {
|
||||
_goTo(content.path);
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _goTo(String path) {
|
||||
_directory = androidFileUtils.relativeDirectoryFromPath(path)!;
|
||||
_contents = null;
|
||||
final contents = <Directory>[];
|
||||
Directory(currentDirectoryPath).list().listen((event) {
|
||||
final entity = event.absolute;
|
||||
if (entity is Directory) {
|
||||
contents.add(entity);
|
||||
}
|
||||
}, onDone: () {
|
||||
_contents = contents..sort((a, b) => compareAsciiUpperCaseNatural(pContext.split(a.path).last, pContext.split(b.path).last));
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
enum _PickerAction { toggleHiddenView }
|
|
@ -170,8 +170,8 @@ class _SettingsMobilePageState extends State<SettingsMobilePage> with FeedbackMi
|
|||
return item.import(importable[item], source);
|
||||
});
|
||||
showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
|
||||
} catch (error) {
|
||||
debugPrint('failed to import app json, error=$error');
|
||||
} catch (error, stack) {
|
||||
debugPrint('failed to import app json, error=$error\n$stack');
|
||||
showFeedback(context, FeedbackType.warn, context.l10n.genericFailureFeedback);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/filters/rating.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/filters/covered/tag.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
|
@ -204,7 +204,7 @@ class _StatsPageState extends State<StatsPage> with FeedbackMixin, VaultAwareMix
|
|||
..._buildFilterSection<String>(context, l10n.statsTopStatesSectionTitle, _entryCountPerState, (v) => LocationFilter(LocationLevel.state, v)),
|
||||
..._buildFilterSection<String>(context, l10n.statsTopPlacesSectionTitle, _entryCountPerPlace, (v) => LocationFilter(LocationLevel.place, v)),
|
||||
..._buildFilterSection<String>(context, l10n.statsTopTagsSectionTitle, _entryCountPerTag, TagFilter.new),
|
||||
..._buildFilterSection<String>(context, l10n.statsTopAlbumsSectionTitle, _entryCountPerAlbum, (v) => AlbumFilter(v, source.getAlbumDisplayName(context, v))),
|
||||
..._buildFilterSection<String>(context, l10n.statsTopAlbumsSectionTitle, _entryCountPerAlbum, (v) => StoredAlbumFilter(v, source.getStoredAlbumDisplayName(context, v))),
|
||||
if (showRatings) ..._buildFilterSection<int>(context, l10n.searchRatingSectionTitle, _entryCountPerRating, RatingFilter.new, sortByCount: false, maxRowCount: null),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -224,15 +224,16 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
|
|||
}
|
||||
|
||||
Future<void> _convertMotionPhotoToStillImage(BuildContext context, AvesEntry targetEntry) async {
|
||||
final l10n = context.l10n;
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AvesDialog(
|
||||
content: Text(context.l10n.genericDangerWarningDialogMessage),
|
||||
content: Text(l10n.genericDangerWarningDialogMessage),
|
||||
actions: [
|
||||
const CancelButton(),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.maybeOf(context)?.pop(true),
|
||||
child: Text(context.l10n.applyButtonLabel),
|
||||
child: Text(l10n.applyButtonLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'dart:math';
|
|||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/entry/extensions/location.dart';
|
||||
import 'package:aves/model/entry/extensions/props.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/services/media/enums.dart';
|
||||
|
@ -140,7 +140,7 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
settings: const RouteSettings(name: CollectionPage.routeName),
|
||||
builder: (context) => CollectionPage(
|
||||
source: source,
|
||||
filters: {AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum))},
|
||||
filters: {StoredAlbumFilter(destinationAlbum, source.getStoredAlbumDisplayName(context, destinationAlbum))},
|
||||
highlightTest: (entry) => entry.uri == newUri,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -6,12 +6,12 @@ import 'package:aves/model/entry/extensions/favourites.dart';
|
|||
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||
import 'package:aves/model/entry/extensions/props.dart';
|
||||
import 'package:aves/model/favourites.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/date.dart';
|
||||
import 'package:aves/model/filters/favourite.dart';
|
||||
import 'package:aves/model/filters/mime.dart';
|
||||
import 'package:aves/model/filters/rating.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/filters/covered/tag.dart';
|
||||
import 'package:aves/model/filters/type.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
|
@ -131,7 +131,7 @@ class _BasicSectionState extends State<BasicSection> {
|
|||
if (entry.isPureVideo && entry.is360) TypeFilter.sphericalVideo,
|
||||
if (entry.isPureVideo && !entry.is360) MimeFilter.video,
|
||||
if (dateTime != null) DateFilter(DateLevel.ymd, dateTime.date),
|
||||
if (album != null) AlbumFilter(album, collection?.source.getAlbumDisplayName(context, album)),
|
||||
if (album != null) StoredAlbumFilter(album, collection?.source.getStoredAlbumDisplayName(context, album)),
|
||||
if (entry.rating != 0) RatingFilter(entry.rating),
|
||||
...tags.map(TagFilter.new),
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/entry/extensions/location.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/settings/enums/coordinate_format.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/mime.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
|
@ -147,7 +147,7 @@ class _SlideshowPageState extends State<SlideshowPage> {
|
|||
settings: const RouteSettings(name: CollectionPage.routeName),
|
||||
builder: (context) => CollectionPage(
|
||||
source: source,
|
||||
filters: album != null ? {AlbumFilter(album, source.getAlbumDisplayName(context, album))} : null,
|
||||
filters: album != null ? {StoredAlbumFilter(album, source.getStoredAlbumDisplayName(context, album))} : null,
|
||||
highlightTest: (entry) => entry.uri == uri,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -15,6 +15,7 @@ enum ChipSetAction {
|
|||
stats,
|
||||
// selecting (single/multiple filters)
|
||||
delete,
|
||||
remove,
|
||||
hide,
|
||||
pin,
|
||||
unpin,
|
||||
|
@ -54,6 +55,7 @@ class ChipSetActions {
|
|||
ChipSetAction.pin,
|
||||
ChipSetAction.unpin,
|
||||
ChipSetAction.delete,
|
||||
ChipSetAction.remove,
|
||||
ChipSetAction.rename,
|
||||
ChipSetAction.showCountryStates,
|
||||
ChipSetAction.hide,
|
||||
|
|
|
@ -7,6 +7,7 @@ enum EntrySetAction {
|
|||
// browsing
|
||||
searchCollection,
|
||||
toggleTitleSearch,
|
||||
addDynamicAlbum,
|
||||
addShortcut,
|
||||
setHome,
|
||||
emptyBin,
|
||||
|
@ -47,6 +48,7 @@ class EntrySetActions {
|
|||
static const pageBrowsing = [
|
||||
EntrySetAction.searchCollection,
|
||||
EntrySetAction.toggleTitleSearch,
|
||||
EntrySetAction.addDynamicAlbum,
|
||||
EntrySetAction.addShortcut,
|
||||
EntrySetAction.setHome,
|
||||
null,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/db/db.dart';
|
||||
import 'package:aves/model/dynamic_albums.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/favourites.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
|
@ -113,6 +114,9 @@ class FakeAvesDb extends Fake implements LocalMediaDb {
|
|||
@override
|
||||
Future<void> removeCovers(Set<CollectionFilter> filters) => SynchronousFuture(null);
|
||||
|
||||
@override
|
||||
Future<Set<DynamicAlbumRow>> loadAllDynamicAlbums() => SynchronousFuture({});
|
||||
|
||||
// video playback
|
||||
|
||||
@override
|
||||
|
|
|
@ -6,8 +6,8 @@ import 'package:aves/model/covers.dart';
|
|||
import 'package:aves/model/db/db.dart';
|
||||
import 'package:aves/model/entry/extensions/favourites.dart';
|
||||
import 'package:aves/model/favourites.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/covered/tag.dart';
|
||||
import 'package:aves/model/metadata/address.dart';
|
||||
import 'package:aves/model/metadata/catalog.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
|
@ -117,7 +117,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('album/country/tag hidden on launch when their items are hidden by entry prop', () async {
|
||||
settings.hiddenFilters = {AlbumFilter(testAlbum, 'whatever')};
|
||||
settings.hiddenFilters = {StoredAlbumFilter(testAlbum, 'whatever')};
|
||||
|
||||
final image1 = FakeMediaStoreService.newImage(testAlbum, 'image1');
|
||||
(mediaStoreService as FakeMediaStoreService).entries = {
|
||||
|
@ -195,7 +195,7 @@ void main() {
|
|||
expect(source.rawAlbums.length, 1);
|
||||
expect(covers.count, 0);
|
||||
|
||||
final albumFilter = AlbumFilter(testAlbum, 'whatever');
|
||||
final albumFilter = StoredAlbumFilter(testAlbum, 'whatever');
|
||||
expect(albumFilter.test(image1), true);
|
||||
expect(covers.count, 0);
|
||||
expect(covers.of(albumFilter), null);
|
||||
|
@ -217,7 +217,7 @@ void main() {
|
|||
|
||||
final source = await _initSource();
|
||||
await image1.toggleFavourite();
|
||||
final albumFilter = AlbumFilter(testAlbum, 'whatever');
|
||||
final albumFilter = StoredAlbumFilter(testAlbum, 'whatever');
|
||||
await covers.set(filter: albumFilter, entryId: image1.id, packageName: null, color: null);
|
||||
await source.updateAfterRename(
|
||||
todoEntries: {image1},
|
||||
|
@ -241,7 +241,7 @@ void main() {
|
|||
|
||||
final source = await _initSource();
|
||||
await image1.toggleFavourite();
|
||||
final albumFilter = AlbumFilter(image1.directory!, 'whatever');
|
||||
final albumFilter = StoredAlbumFilter(image1.directory!, 'whatever');
|
||||
await covers.set(filter: albumFilter, entryId: image1.id, packageName: null, color: null);
|
||||
await source.removeEntries({image1.uri}, includeTrash: true);
|
||||
|
||||
|
@ -261,8 +261,8 @@ void main() {
|
|||
expect(source.rawAlbums.contains(sourceAlbum), true);
|
||||
expect(source.rawAlbums.contains(destinationAlbum), false);
|
||||
|
||||
final sourceAlbumFilter = AlbumFilter(sourceAlbum, 'whatever');
|
||||
final destinationAlbumFilter = AlbumFilter(destinationAlbum, 'whatever');
|
||||
final sourceAlbumFilter = StoredAlbumFilter(sourceAlbum, 'whatever');
|
||||
final destinationAlbumFilter = StoredAlbumFilter(destinationAlbum, 'whatever');
|
||||
expect(sourceAlbumFilter.test(image1), true);
|
||||
expect(destinationAlbumFilter.test(image1), false);
|
||||
|
||||
|
@ -312,7 +312,7 @@ void main() {
|
|||
|
||||
final source = await _initSource();
|
||||
expect(source.rawAlbums.length, 1);
|
||||
final sourceAlbumFilter = AlbumFilter(sourceAlbum, 'whatever');
|
||||
final sourceAlbumFilter = StoredAlbumFilter(sourceAlbum, 'whatever');
|
||||
await covers.set(filter: sourceAlbumFilter, entryId: image1.id, packageName: null, color: null);
|
||||
|
||||
await source.updateAfterMove(
|
||||
|
@ -337,14 +337,14 @@ void main() {
|
|||
|
||||
final source = await _initSource();
|
||||
await image1.toggleFavourite();
|
||||
var albumFilter = AlbumFilter(sourceAlbum, 'whatever');
|
||||
var albumFilter = StoredAlbumFilter(sourceAlbum, 'whatever');
|
||||
await covers.set(filter: albumFilter, entryId: image1.id, packageName: null, color: null);
|
||||
await source.renameAlbum(sourceAlbum, destinationAlbum, {
|
||||
await source.renameStoredAlbum(sourceAlbum, destinationAlbum, {
|
||||
image1
|
||||
}, {
|
||||
FakeMediaStoreService.moveOpEventForMove(image1, sourceAlbum, destinationAlbum),
|
||||
});
|
||||
albumFilter = AlbumFilter(destinationAlbum, 'whatever');
|
||||
albumFilter = StoredAlbumFilter(destinationAlbum, 'whatever');
|
||||
|
||||
expect(favourites.count, 1);
|
||||
expect(image1.isFavourite, true);
|
||||
|
@ -377,20 +377,20 @@ void main() {
|
|||
delegates: AppLocalizations.localizationsDelegates,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
expect(source.getAlbumDisplayName(context, '${FakeStorageService.primaryPath}Pictures/Elea/Zeno'), 'Elea/Zeno');
|
||||
expect(source.getAlbumDisplayName(context, '${FakeStorageService.primaryPath}Pictures/Citium/Zeno'), 'Citium/Zeno');
|
||||
expect(source.getAlbumDisplayName(context, '${FakeStorageService.primaryPath}Pictures/Cleanthes'), 'Cleanthes');
|
||||
expect(source.getAlbumDisplayName(context, '${FakeStorageService.primaryPath}Pictures/Chrysippus'), 'Chrysippus');
|
||||
expect(source.getAlbumDisplayName(context, '${FakeStorageService.removablePath}Pictures/Chrysippus'), 'Chrysippus (${FakeStorageService.removableDescription})');
|
||||
expect(source.getAlbumDisplayName(context, FakeStorageService.primaryRootAlbum), FakeStorageService.primaryDescription);
|
||||
expect(source.getAlbumDisplayName(context, '${FakeStorageService.primaryPath}Pictures/Seneca'), 'Pictures/Seneca');
|
||||
expect(source.getAlbumDisplayName(context, '${FakeStorageService.primaryPath}Seneca'), 'Seneca');
|
||||
expect(source.getAlbumDisplayName(context, '${FakeStorageService.removablePath}Pictures/Cicero'), 'Cicero');
|
||||
expect(source.getAlbumDisplayName(context, '${FakeStorageService.removablePath}Marcus Aurelius'), 'Marcus Aurelius');
|
||||
expect(source.getAlbumDisplayName(context, '${FakeStorageService.primaryPath}Pictures/Hannah Arendt'), 'Hannah Arendt');
|
||||
expect(source.getAlbumDisplayName(context, '${FakeStorageService.primaryPath}Pictures/Arendt'), 'Arendt');
|
||||
expect(source.getAlbumDisplayName(context, '${FakeStorageService.primaryPath}Pictures/Something'), 'Pictures/Something');
|
||||
expect(source.getAlbumDisplayName(context, '${FakeStorageService.primaryPath}Movies/SomeThing'), 'Movies/SomeThing');
|
||||
expect(source.getStoredAlbumDisplayName(context, '${FakeStorageService.primaryPath}Pictures/Elea/Zeno'), 'Elea/Zeno');
|
||||
expect(source.getStoredAlbumDisplayName(context, '${FakeStorageService.primaryPath}Pictures/Citium/Zeno'), 'Citium/Zeno');
|
||||
expect(source.getStoredAlbumDisplayName(context, '${FakeStorageService.primaryPath}Pictures/Cleanthes'), 'Cleanthes');
|
||||
expect(source.getStoredAlbumDisplayName(context, '${FakeStorageService.primaryPath}Pictures/Chrysippus'), 'Chrysippus');
|
||||
expect(source.getStoredAlbumDisplayName(context, '${FakeStorageService.removablePath}Pictures/Chrysippus'), 'Chrysippus (${FakeStorageService.removableDescription})');
|
||||
expect(source.getStoredAlbumDisplayName(context, FakeStorageService.primaryRootAlbum), FakeStorageService.primaryDescription);
|
||||
expect(source.getStoredAlbumDisplayName(context, '${FakeStorageService.primaryPath}Pictures/Seneca'), 'Pictures/Seneca');
|
||||
expect(source.getStoredAlbumDisplayName(context, '${FakeStorageService.primaryPath}Seneca'), 'Seneca');
|
||||
expect(source.getStoredAlbumDisplayName(context, '${FakeStorageService.removablePath}Pictures/Cicero'), 'Cicero');
|
||||
expect(source.getStoredAlbumDisplayName(context, '${FakeStorageService.removablePath}Marcus Aurelius'), 'Marcus Aurelius');
|
||||
expect(source.getStoredAlbumDisplayName(context, '${FakeStorageService.primaryPath}Pictures/Hannah Arendt'), 'Hannah Arendt');
|
||||
expect(source.getStoredAlbumDisplayName(context, '${FakeStorageService.primaryPath}Pictures/Arendt'), 'Arendt');
|
||||
expect(source.getStoredAlbumDisplayName(context, '${FakeStorageService.primaryPath}Pictures/Something'), 'Pictures/Something');
|
||||
expect(source.getStoredAlbumDisplayName(context, '${FakeStorageService.primaryPath}Movies/SomeThing'), 'Movies/SomeThing');
|
||||
return const Placeholder();
|
||||
},
|
||||
),
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||
import 'package:aves/model/filters/aspect_ratio.dart';
|
||||
import 'package:aves/model/filters/coordinate.dart';
|
||||
import 'package:aves/model/filters/date.dart';
|
||||
import 'package:aves/model/filters/favourite.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/covered/location.dart';
|
||||
import 'package:aves/model/filters/mime.dart';
|
||||
import 'package:aves/model/filters/missing.dart';
|
||||
import 'package:aves/model/filters/path.dart';
|
||||
|
@ -12,7 +12,7 @@ import 'package:aves/model/filters/placeholder.dart';
|
|||
import 'package:aves/model/filters/query.dart';
|
||||
import 'package:aves/model/filters/rating.dart';
|
||||
import 'package:aves/model/filters/recent.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/filters/covered/tag.dart';
|
||||
import 'package:aves/model/filters/type.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
@ -35,7 +35,7 @@ void main() {
|
|||
test('Filter serialization', () {
|
||||
CollectionFilter? jsonRoundTrip(filter) => CollectionFilter.fromJson(filter.toJson());
|
||||
|
||||
final album = AlbumFilter('path/to/album', 'album');
|
||||
final album = StoredAlbumFilter('path/to/album', 'album');
|
||||
expect(album, jsonRoundTrip(album));
|
||||
|
||||
final aspectRatio = AspectRatioFilter.landscape;
|
||||
|
|
Loading…
Reference in a new issue