529 lines
14 KiB
Dart
529 lines
14 KiB
Dart
// lib/model/source/album.dart
|
|
import 'package:aves/model/db/folder_stats.dart';
|
|
import 'package:aves/model/entry/entry.dart';
|
|
import 'package:aves/model/filters/container/album_group.dart';
|
|
import 'package:aves/model/filters/container/dynamic_album.dart';
|
|
import 'package:aves/model/filters/covered/stored_album.dart';
|
|
import 'package:aves/model/settings/settings.dart';
|
|
import 'package:aves/model/source/collection_source.dart';
|
|
import 'package:aves/model/vaults/vaults.dart';
|
|
import 'package:aves/services/common/services.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_model/aves_model.dart';
|
|
import 'package:collection/collection.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
|
|
bool debugCreateFoldersEnabled = false;
|
|
|
|
// ============================================================
|
|
// FASE 2: FolderRow legacy
|
|
// Conservata per compatibilità, ma la logica moderna usa FolderStats.
|
|
// ============================================================
|
|
|
|
class FolderRow {
|
|
final String path;
|
|
final int albumType;
|
|
final int priority;
|
|
final String displayName;
|
|
|
|
final int? isRemote;
|
|
final int? hybridType;
|
|
final String? deviceId;
|
|
|
|
FolderRow({
|
|
required this.path,
|
|
required this.albumType,
|
|
required this.priority,
|
|
required this.displayName,
|
|
this.isRemote,
|
|
this.hybridType,
|
|
this.deviceId,
|
|
});
|
|
}
|
|
|
|
// ============================================================
|
|
|
|
mixin AlbumMixin on SourceBase {
|
|
final Set<String> _directories = {};
|
|
final Set<String> _newAlbums = {};
|
|
|
|
// Cache cartelle moderna, basata esclusivamente su folderMap.
|
|
late Map<String, FolderStats> foldersCache = {};
|
|
|
|
bool _albumsReadyNotified = false;
|
|
|
|
// ============================================================
|
|
// GROUPING ALBUM REMOTI / LOCALI
|
|
// ============================================================
|
|
|
|
String groupForAlbum(String dir) {
|
|
final folder = foldersCache[dir];
|
|
|
|
if (folder == null) return 'Local';
|
|
|
|
// Gli album ibridi sono trattati come Local.
|
|
if (folder.hybridType != null) return 'Local';
|
|
|
|
if (folder.isRemote == 1) return 'Remote';
|
|
|
|
return 'Local';
|
|
}
|
|
|
|
static const groupOrder = {
|
|
'Local': 0,
|
|
'User': 1,
|
|
'Common': 2,
|
|
// Device:<id> va sempre in fondo.
|
|
};
|
|
|
|
int orderForGroup(String g) {
|
|
if (groupOrder.containsKey(g)) return groupOrder[g]!;
|
|
|
|
if (g.startsWith('User:')) return 1;
|
|
if (g.startsWith('Device:')) return 3;
|
|
|
|
return 4;
|
|
}
|
|
|
|
// ============================================================
|
|
// Rebuild da folderMap only
|
|
// ============================================================
|
|
|
|
void _rebuildDirectoriesFromFolderMap({String reason = 'unknown'}) {
|
|
foldersCache = Map<String, FolderStats>.from(folderMap);
|
|
|
|
final grouped = <String, List<String>>{};
|
|
|
|
for (final dir in foldersCache.keys) {
|
|
final folder = foldersCache[dir];
|
|
|
|
if (folder?.hidden == 1) continue;
|
|
|
|
if (!remoteVisible && folder?.isRemote == 1) continue;
|
|
|
|
final group = groupForAlbum(dir);
|
|
grouped.putIfAbsent(group, () => <String>[]).add(dir);
|
|
}
|
|
|
|
final sortedGroups = grouped.keys.toList()
|
|
..sort((a, b) => orderForGroup(a).compareTo(orderForGroup(b)));
|
|
|
|
final sortedDirs = <String>[];
|
|
|
|
for (final group in sortedGroups) {
|
|
final dirs = grouped[group]!;
|
|
|
|
dirs.sort((a, b) {
|
|
final fa = foldersCache[a];
|
|
final fb = foldersCache[b];
|
|
|
|
if (fa != null && fb != null) {
|
|
final p = fa.priority.compareTo(fb.priority);
|
|
if (p != 0) return p;
|
|
return compareAsciiUpperCaseNatural(fa.displayName, fb.displayName);
|
|
}
|
|
|
|
return compareAsciiUpperCaseNatural(a, b);
|
|
});
|
|
|
|
sortedDirs.addAll(dirs);
|
|
}
|
|
|
|
_directories
|
|
..clear()
|
|
..addAll(sortedDirs);
|
|
|
|
debugPrint(
|
|
'[albums] rebuildFromFolderMap '
|
|
'reason=$reason '
|
|
'folderMap=${folderMap.length} '
|
|
'foldersCache=${foldersCache.length} '
|
|
'directories=${_directories.length}',
|
|
);
|
|
}
|
|
|
|
// ============================================================
|
|
// Caricamento cartelle
|
|
// ============================================================
|
|
|
|
@override
|
|
Future<Map<String, FolderStats>> loadFoldersFromDb() async {
|
|
_rebuildDirectoriesFromFolderMap(reason: 'loadFoldersFromDb');
|
|
|
|
return Map<String, FolderStats>.from(foldersCache);
|
|
}
|
|
|
|
@override
|
|
Future<void> initFoldersCache() async {
|
|
_rebuildDirectoriesFromFolderMap(reason: 'initFoldersCache');
|
|
}
|
|
|
|
List<String> get rawAlbums {
|
|
if (_directories.isEmpty && folderMap.isNotEmpty) {
|
|
_rebuildDirectoriesFromFolderMap(reason: 'rawAlbums-empty');
|
|
}
|
|
|
|
if (!_albumsReadyNotified && _directories.isNotEmpty) {
|
|
_albumsReadyNotified = true;
|
|
|
|
Future.microtask(() {
|
|
invalidateStoredAlbumDisplayNames();
|
|
notifyAlbumsChanged();
|
|
|
|
debugPrint(
|
|
'[albums] rawAlbums notified UI refresh '
|
|
'count=${_directories.length}',
|
|
);
|
|
});
|
|
}
|
|
|
|
debugPrint(
|
|
'[albums] rawAlbums '
|
|
'count=${_directories.length} '
|
|
'folderMap=${folderMap.length}',
|
|
);
|
|
|
|
return List.unmodifiable(_directories);
|
|
}
|
|
|
|
Set<StoredAlbumFilter> getNewAlbumFilters(BuildContext context) =>
|
|
Set.unmodifiable(
|
|
_newAlbums.map(
|
|
(v) => StoredAlbumFilter(
|
|
v,
|
|
getStoredAlbumDisplayName(context, v),
|
|
),
|
|
),
|
|
);
|
|
|
|
// ============================================================
|
|
// Ordinamento album
|
|
// ============================================================
|
|
|
|
int compareAlbumsByPath(String? a, String? b) =>
|
|
compareAsciiUpperCase(a ??= '', b ??= '');
|
|
|
|
@override
|
|
int compareAlbumsByName(String? a, String? b) {
|
|
final fa = foldersCache[a];
|
|
final fb = foldersCache[b];
|
|
|
|
if (fa != null && fb != null) {
|
|
final p = fa.priority.compareTo(fb.priority);
|
|
if (p != 0) return p;
|
|
return compareAsciiUpperCaseNatural(fa.displayName, fb.displayName);
|
|
}
|
|
|
|
return compareAsciiUpperCaseNatural(a ?? '', b ?? '');
|
|
}
|
|
|
|
void notifyAlbumsChanged() {
|
|
eventBus.fire(AlbumsChangedEvent());
|
|
}
|
|
|
|
void _onAlbumChanged({bool notify = true}) {
|
|
invalidateStoredAlbumDisplayNames();
|
|
|
|
if (notify) {
|
|
notifyAlbumsChanged();
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// getAlbumEntries
|
|
// ============================================================
|
|
|
|
Map<String, AvesEntry?> getAlbumEntries() {
|
|
if (_directories.isEmpty && folderMap.isNotEmpty) {
|
|
_rebuildDirectoriesFromFolderMap(reason: 'getAlbumEntries-empty');
|
|
}
|
|
|
|
final entries = sortedEntriesByDate;
|
|
final result = <String, AvesEntry?>{};
|
|
|
|
for (final album in rawAlbums) {
|
|
result[album] = entries.firstWhereOrNull(
|
|
(entry) => entry.directory == album,
|
|
);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// ============================================================
|
|
// updateDirectories
|
|
// ============================================================
|
|
|
|
@override
|
|
Future<void> updateDirectories() async {
|
|
_rebuildDirectoriesFromFolderMap(reason: 'updateDirectories');
|
|
|
|
_albumsReadyNotified = true;
|
|
invalidateStoredAlbumDisplayNames();
|
|
|
|
notifyAlbumsChanged();
|
|
}
|
|
|
|
// ============================================================
|
|
// addDirectories / cleanEmptyAlbums
|
|
//
|
|
// FolderMap-only:
|
|
// - non scriviamo folders da qui;
|
|
// - computeFolderMap() in CollectionSource è la fonte unica.
|
|
// ============================================================
|
|
|
|
@override
|
|
void addDirectories({
|
|
required Set<String?> albums,
|
|
bool notify = true,
|
|
}) async {
|
|
final dirs = albums.nonNulls.where((a) => a.isNotEmpty).toSet();
|
|
if (dirs.isEmpty) return;
|
|
|
|
if (!debugCreateFoldersEnabled) {
|
|
debugPrint(
|
|
'[folders] CREATION SKIPPED (${dirs.length}) '
|
|
'- folderMap-only mode',
|
|
);
|
|
}
|
|
|
|
// Non modifichiamo foldersCache manualmente.
|
|
// Le nuove cartelle appariranno quando computeFolderMap()
|
|
// aggiornerà source.folderMap.
|
|
if (notify) {
|
|
notifyAlbumsChanged();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void cleanEmptyAlbums([Set<String>? albums]) async {
|
|
final dirs = (albums ?? _directories).nonNulls.toSet();
|
|
if (dirs.isEmpty) return;
|
|
|
|
bool changed = false;
|
|
|
|
for (final dir in dirs) {
|
|
final stillHasEntries = visibleEntries.any((e) => e.directory == dir);
|
|
|
|
if (!stillHasEntries && _isRemovable(dir)) {
|
|
foldersCache.remove(dir);
|
|
_directories.remove(dir);
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
notifyAlbumsChanged();
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
|
|
bool _isRemovable(String album) {
|
|
if (visibleEntries.any((entry) => entry.directory == album)) return false;
|
|
if (_newAlbums.contains(album)) return false;
|
|
if (vaults.isVault(album)) return false;
|
|
|
|
if (settings.pinnedFilters
|
|
.whereType<StoredAlbumFilter>()
|
|
.map((v) => v.album)
|
|
.contains(album)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// ============================================================
|
|
// Filter summary
|
|
// ============================================================
|
|
|
|
final Map<String, int> _filterEntryCountMap = {}, _filterSizeMap = {};
|
|
final Map<String, AvesEntry?> _filterRecentEntryMap = {};
|
|
|
|
void invalidateAlbumFilterSummary({
|
|
Set<AvesEntry>? entries,
|
|
Set<String?>? directories,
|
|
bool notify = true,
|
|
}) {
|
|
if (_filterEntryCountMap.isEmpty &&
|
|
_filterSizeMap.isEmpty &&
|
|
_filterRecentEntryMap.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
if (entries == null && directories == null) {
|
|
_filterEntryCountMap.clear();
|
|
_filterSizeMap.clear();
|
|
_filterRecentEntryMap.clear();
|
|
} else {
|
|
directories ??= {};
|
|
|
|
if (entries != null) {
|
|
directories.addAll(entries.map((entry) => entry.directory).nonNulls);
|
|
}
|
|
|
|
directories.nonNulls
|
|
.map((v) => StoredAlbumFilter(v, null).key)
|
|
.forEach((key) {
|
|
_filterEntryCountMap.remove(key);
|
|
_filterSizeMap.remove(key);
|
|
_filterRecentEntryMap.remove(key);
|
|
});
|
|
|
|
invalidateDynamicAlbumFilterSummary(notify: false);
|
|
invalidateAlbumGroupFilterSummary(notify: false);
|
|
}
|
|
|
|
if (notify) {
|
|
eventBus.fire(StoredAlbumSummaryInvalidatedEvent(directories));
|
|
eventBus.fire(const DynamicAlbumSummaryInvalidatedEvent());
|
|
eventBus.fire(const AlbumGroupSummaryInvalidatedEvent());
|
|
}
|
|
}
|
|
|
|
void invalidateDynamicAlbumFilterSummary({bool notify = true}) {
|
|
_filterEntryCountMap.removeWhere(_isDynamicAlbumKey);
|
|
_filterSizeMap.removeWhere(_isDynamicAlbumKey);
|
|
_filterRecentEntryMap.removeWhere(_isDynamicAlbumKey);
|
|
|
|
if (notify) {
|
|
eventBus.fire(const DynamicAlbumSummaryInvalidatedEvent());
|
|
}
|
|
}
|
|
|
|
void invalidateAlbumGroupFilterSummary({bool notify = true}) {
|
|
_filterEntryCountMap.removeWhere(_isAlbumGroupKey);
|
|
_filterSizeMap.removeWhere(_isAlbumGroupKey);
|
|
_filterRecentEntryMap.removeWhere(_isAlbumGroupKey);
|
|
|
|
if (notify) {
|
|
eventBus.fire(const AlbumGroupSummaryInvalidatedEvent());
|
|
}
|
|
}
|
|
|
|
bool _isDynamicAlbumKey(String key, _) =>
|
|
key.startsWith('${DynamicAlbumFilter.type}-');
|
|
|
|
bool _isAlbumGroupKey(String key, _) =>
|
|
key.startsWith('${AlbumGroupFilter.type}-');
|
|
|
|
// ============================================================
|
|
// Album summary
|
|
// Compatibile con Aves: UI usa StoredAlbumFilter,
|
|
// ma la lista album viene da folderMap.
|
|
// ============================================================
|
|
|
|
int albumEntryCount(AlbumBaseFilter filter) {
|
|
return _filterEntryCountMap.putIfAbsent(
|
|
filter.key,
|
|
() {
|
|
if (filter is StoredAlbumFilter) {
|
|
final album = filter.album;
|
|
|
|
return visibleEntries
|
|
.where((entry) => entry.directory == album)
|
|
.length;
|
|
}
|
|
|
|
return visibleEntries.where(filter.test).length;
|
|
},
|
|
);
|
|
}
|
|
|
|
int albumSize(AlbumBaseFilter filter) {
|
|
return _filterSizeMap.putIfAbsent(
|
|
filter.key,
|
|
() {
|
|
if (filter is StoredAlbumFilter) {
|
|
final album = filter.album;
|
|
|
|
return visibleEntries
|
|
.where((entry) => entry.directory == album)
|
|
.map((v) => v.sizeBytes)
|
|
.sum;
|
|
}
|
|
|
|
return visibleEntries.where(filter.test).map((v) => v.sizeBytes).sum;
|
|
},
|
|
);
|
|
}
|
|
|
|
AvesEntry? albumRecentEntry(AlbumBaseFilter filter) {
|
|
return _filterRecentEntryMap.putIfAbsent(
|
|
filter.key,
|
|
() {
|
|
if (filter is StoredAlbumFilter) {
|
|
final album = filter.album;
|
|
|
|
return sortedEntriesByDate.firstWhereOrNull(
|
|
(entry) => entry.directory == album,
|
|
);
|
|
}
|
|
|
|
return sortedEntriesByDate.firstWhereOrNull(filter.test);
|
|
},
|
|
);
|
|
}
|
|
|
|
// ============================================================
|
|
// New albums
|
|
// ============================================================
|
|
|
|
void createStoredAlbum(String directory) {
|
|
if (!_directories.contains(directory)) {
|
|
_newAlbums.add(directory);
|
|
addDirectories(albums: {directory});
|
|
}
|
|
}
|
|
|
|
void renameNewAlbum(String source, String destination) {
|
|
if (_newAlbums.remove(source)) {
|
|
cleanEmptyAlbums({source});
|
|
createStoredAlbum(destination);
|
|
}
|
|
}
|
|
|
|
void forgetNewAlbums(Set<String> directories) {
|
|
_newAlbums.removeAll(directories);
|
|
}
|
|
|
|
// ============================================================
|
|
// Display names
|
|
// ============================================================
|
|
|
|
final Map<String, String> _albumDisplayNamesWithContext = {},
|
|
_albumDisplayNamesWithoutContext = {};
|
|
|
|
void invalidateStoredAlbumDisplayNames() {
|
|
_albumDisplayNamesWithContext.clear();
|
|
_albumDisplayNamesWithoutContext.clear();
|
|
}
|
|
|
|
@override
|
|
String getStoredAlbumDisplayName(BuildContext? context, String dirPath) {
|
|
final f = foldersCache[dirPath] ?? folderMap[dirPath];
|
|
|
|
if (f != null) return f.displayName;
|
|
|
|
return pContext.basename(dirPath);
|
|
}
|
|
}
|
|
|
|
class AlbumsChangedEvent {}
|
|
|
|
class DynamicAlbumSummaryInvalidatedEvent {
|
|
const DynamicAlbumSummaryInvalidatedEvent();
|
|
}
|
|
|
|
class AlbumGroupSummaryInvalidatedEvent {
|
|
const AlbumGroupSummaryInvalidatedEvent();
|
|
}
|
|
|
|
class StoredAlbumSummaryInvalidatedEvent {
|
|
final Set<String?>? directories;
|
|
|
|
const StoredAlbumSummaryInvalidatedEvent(this.directories);
|
|
}
|