// 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 _directories = {}; final Set _newAlbums = {}; // Cache cartelle moderna, basata esclusivamente su folderMap. late Map 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: 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.from(folderMap); final grouped = >{}; for (final dir in foldersCache.keys) { final folder = foldersCache[dir]; if (folder?.hidden == 1) continue; final group = groupForAlbum(dir); grouped.putIfAbsent(group, () => []).add(dir); } final sortedGroups = grouped.keys.toList() ..sort((a, b) => orderForGroup(a).compareTo(orderForGroup(b))); final sortedDirs = []; 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> loadFoldersFromDb() async { _rebuildDirectoriesFromFolderMap(reason: 'loadFoldersFromDb'); return Map.from(foldersCache); } @override Future initFoldersCache() async { _rebuildDirectoriesFromFolderMap(reason: 'initFoldersCache'); } List 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 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 getAlbumEntries() { if (_directories.isEmpty && folderMap.isNotEmpty) { _rebuildDirectoriesFromFolderMap(reason: 'getAlbumEntries-empty'); } final entries = sortedEntriesByDate; final result = {}; for (final album in rawAlbums) { result[album] = entries.firstWhereOrNull( (entry) => entry.directory == album, ); } return result; } // ============================================================ // updateDirectories // ============================================================ @override Future 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 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? 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() .map((v) => v.album) .contains(album)) { return false; } return true; } // ============================================================ // Filter summary // ============================================================ final Map _filterEntryCountMap = {}, _filterSizeMap = {}; final Map _filterRecentEntryMap = {}; void invalidateAlbumFilterSummary({ Set? entries, Set? 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 directories) { _newAlbums.removeAll(directories); } // ============================================================ // Display names // ============================================================ final Map _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? directories; const StoredAlbumSummaryInvalidatedEvent(this.directories); }