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