aves_mio100/lib/model/source/album.dart.ok
2026-07-06 14:52:45 +02:00

527 lines
14 KiB
Text

// 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;
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);
}