diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart index fb5479a548..1639a47dbb 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -87,7 +87,7 @@ enum StoreKey { // Free up space cleanupKeepFavorites._(1008), cleanupKeepMediaType._(1009), - cleanupExcludedAlbumIds._(1010), + cleanupKeepAlbumIds._(1010), cleanupCutoffDaysAgo._(1011); const StoreKey._(this.id); diff --git a/mobile/lib/infrastructure/repositories/local_asset.repository.dart b/mobile/lib/infrastructure/repositories/local_asset.repository.dart index a4c98dae48..4c78aea825 100644 --- a/mobile/lib/infrastructure/repositories/local_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_asset.repository.dart @@ -135,7 +135,7 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { DateTime cutoffDate, { AssetKeepType keepMediaType = AssetKeepType.none, bool keepFavorites = true, - Set excludedAlbumIds = const {}, + Set keepAlbumIds = const {}, }) async { final iosSharedAlbumAssets = _db.localAlbumAssetEntity.selectOnly() ..addColumns([_db.localAlbumAssetEntity.assetId]) @@ -160,14 +160,13 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { // Exclude assets that are in iOS shared albums whereClause = whereClause & _db.localAssetEntity.id.isNotInQuery(iosSharedAlbumAssets); - if (excludedAlbumIds.isNotEmpty) { - final excludedAlbumAssets = _db.localAlbumAssetEntity.selectOnly() + if (keepAlbumIds.isNotEmpty) { + final keepAlbumAssets = _db.localAlbumAssetEntity.selectOnly() ..addColumns([_db.localAlbumAssetEntity.assetId]) - ..where(_db.localAlbumAssetEntity.albumId.isIn(excludedAlbumIds)); - whereClause = whereClause & _db.localAssetEntity.id.isNotInQuery(excludedAlbumAssets); + ..where(_db.localAlbumAssetEntity.albumId.isIn(keepAlbumIds)); + whereClause = whereClause & _db.localAssetEntity.id.isNotInQuery(keepAlbumAssets); } - // keepMediaType specifies what to KEEP, so we filter to DELETE the opposite if (keepMediaType == AssetKeepType.photosOnly) { // Keep photos = delete only videos whereClause = whereClause & _db.localAssetEntity.type.equalsValue(AssetType.video); diff --git a/mobile/lib/providers/cleanup.provider.dart b/mobile/lib/providers/cleanup.provider.dart index d0d13715f7..081a6e2c8c 100644 --- a/mobile/lib/providers/cleanup.provider.dart +++ b/mobile/lib/providers/cleanup.provider.dart @@ -13,7 +13,7 @@ class CleanupState { final bool isDeleting; final AssetKeepType keepMediaType; final bool keepFavorites; - final Set excludedAlbumIds; + final Set keepAlbumIds; const CleanupState({ this.selectedDate, @@ -22,7 +22,7 @@ class CleanupState { this.isDeleting = false, this.keepMediaType = AssetKeepType.none, this.keepFavorites = true, - this.excludedAlbumIds = const {}, + this.keepAlbumIds = const {}, }); CleanupState copyWith({ @@ -32,7 +32,7 @@ class CleanupState { bool? isDeleting, AssetKeepType? keepMediaType, bool? keepFavorites, - Set? excludedAlbumIds, + Set? keepAlbumIds, }) { return CleanupState( selectedDate: selectedDate ?? this.selectedDate, @@ -41,7 +41,7 @@ class CleanupState { isDeleting: isDeleting ?? this.isDeleting, keepMediaType: keepMediaType ?? this.keepMediaType, keepFavorites: keepFavorites ?? this.keepFavorites, - excludedAlbumIds: excludedAlbumIds ?? this.excludedAlbumIds, + keepAlbumIds: keepAlbumIds ?? this.keepAlbumIds, ); } } @@ -66,17 +66,17 @@ class CleanupNotifier extends StateNotifier { void _loadPersistedSettings() { final keepFavorites = _appSettingsService.getSetting(AppSettingsEnum.cleanupKeepFavorites); final keepMediaTypeIndex = _appSettingsService.getSetting(AppSettingsEnum.cleanupKeepMediaType); - final excludedAlbumIdsString = _appSettingsService.getSetting(AppSettingsEnum.cleanupExcludedAlbumIds); + final keepAlbumIdsString = _appSettingsService.getSetting(AppSettingsEnum.cleanupKeepAlbumIds); final cutoffDaysAgo = _appSettingsService.getSetting(AppSettingsEnum.cleanupCutoffDaysAgo); final keepMediaType = AssetKeepType.values[keepMediaTypeIndex.clamp(0, AssetKeepType.values.length - 1)]; - final excludedAlbumIds = excludedAlbumIdsString.isEmpty ? {} : excludedAlbumIdsString.split(',').toSet(); + final keepAlbumIds = keepAlbumIdsString.isEmpty ? {} : keepAlbumIdsString.split(',').toSet(); final selectedDate = cutoffDaysAgo > 0 ? DateTime.now().subtract(Duration(days: cutoffDaysAgo)) : null; state = state.copyWith( keepFavorites: keepFavorites, keepMediaType: keepMediaType, - excludedAlbumIds: excludedAlbumIds, + keepAlbumIds: keepAlbumIds, selectedDate: selectedDate, ); } @@ -99,24 +99,24 @@ class CleanupNotifier extends StateNotifier { _appSettingsService.setSetting(AppSettingsEnum.cleanupKeepFavorites, keepFavorites); } - void toggleExcludedAlbum(String albumId) { - final newExcludedAlbumIds = Set.from(state.excludedAlbumIds); - if (newExcludedAlbumIds.contains(albumId)) { - newExcludedAlbumIds.remove(albumId); + void toggleKeepAlbum(String albumId) { + final newKeepAlbumIds = Set.from(state.keepAlbumIds); + if (newKeepAlbumIds.contains(albumId)) { + newKeepAlbumIds.remove(albumId); } else { - newExcludedAlbumIds.add(albumId); + newKeepAlbumIds.add(albumId); } - state = state.copyWith(excludedAlbumIds: newExcludedAlbumIds, assetsToDelete: []); - _persistExcludedAlbumIds(newExcludedAlbumIds); + state = state.copyWith(keepAlbumIds: newKeepAlbumIds, assetsToDelete: []); + _persistExcludedAlbumIds(newKeepAlbumIds); } void setExcludedAlbumIds(Set albumIds) { - state = state.copyWith(excludedAlbumIds: albumIds, assetsToDelete: []); + state = state.copyWith(keepAlbumIds: albumIds, assetsToDelete: []); _persistExcludedAlbumIds(albumIds); } void _persistExcludedAlbumIds(Set albumIds) { - _appSettingsService.setSetting(AppSettingsEnum.cleanupExcludedAlbumIds, albumIds.join(',')); + _appSettingsService.setSetting(AppSettingsEnum.cleanupKeepAlbumIds, albumIds.join(',')); } Future scanAssets() async { @@ -131,7 +131,7 @@ class CleanupNotifier extends StateNotifier { state.selectedDate!, keepMediaType: state.keepMediaType, keepFavorites: state.keepFavorites, - excludedAlbumIds: state.excludedAlbumIds, + keepAlbumIds: state.keepAlbumIds, ); state = state.copyWith(assetsToDelete: assets, isScanning: false); } catch (e) { diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index f4ee3b36f4..f6c28d0bc9 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -57,7 +57,7 @@ enum AppSettingsEnum { backupTriggerDelay(StoreKey.backupTriggerDelay, null, 30), cleanupKeepFavorites(StoreKey.cleanupKeepFavorites, null, true), cleanupKeepMediaType(StoreKey.cleanupKeepMediaType, null, 0), - cleanupExcludedAlbumIds(StoreKey.cleanupExcludedAlbumIds, null, ""), + cleanupKeepAlbumIds(StoreKey.cleanupKeepAlbumIds, null, ""), cleanupCutoffDaysAgo(StoreKey.cleanupCutoffDaysAgo, null, 60); const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); diff --git a/mobile/lib/services/cleanup.service.dart b/mobile/lib/services/cleanup.service.dart index 792e04228d..f6f590dcd0 100644 --- a/mobile/lib/services/cleanup.service.dart +++ b/mobile/lib/services/cleanup.service.dart @@ -20,14 +20,14 @@ class CleanupService { DateTime cutoffDate, { AssetKeepType keepMediaType = AssetKeepType.none, bool keepFavorites = true, - Set excludedAlbumIds = const {}, + Set keepAlbumIds = const {}, }) { return _localAssetRepository.getRemovalCandidates( userId, cutoffDate, keepMediaType: keepMediaType, keepFavorites: keepFavorites, - excludedAlbumIds: excludedAlbumIds, + keepAlbumIds: keepAlbumIds, ); } diff --git a/mobile/lib/widgets/settings/free_up_space_settings.dart b/mobile/lib/widgets/settings/free_up_space_settings.dart index 7a77eae551..ae9487b5eb 100644 --- a/mobile/lib/widgets/settings/free_up_space_settings.dart +++ b/mobile/lib/widgets/settings/free_up_space_settings.dart @@ -150,6 +150,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { final subtitleStyle = context.textTheme.bodyMedium!.copyWith( color: context.textTheme.bodyMedium!.color!.withAlpha(215), ); + StepStyle styleForState(StepState stepState, {bool isDestructive = false}) { switch (stepState) { case StepState.complete: @@ -187,7 +188,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { final step3State = hasAssets ? StepState.indexed : StepState.disabled; final hasKeepSettings = - state.keepFavorites || state.excludedAlbumIds.isNotEmpty || state.keepMediaType != AssetKeepType.none; + state.keepFavorites || state.keepAlbumIds.isNotEmpty || state.keepMediaType != AssetKeepType.none; String getKeepSettingsSummary() { final parts = []; @@ -202,10 +203,8 @@ class _FreeUpSpaceSettingsState extends ConsumerState { parts.add('favorites'.t(context: context).toLowerCase()); } - if (state.excludedAlbumIds.isNotEmpty) { - parts.add( - 'excluded_albums_count'.t(context: context, args: {'count': state.excludedAlbumIds.length.toString()}), - ); + if (state.keepAlbumIds.isNotEmpty) { + parts.add('keep_albums_count'.t(context: context, args: {'count': state.keepAlbumIds.length.toString()})); } if (parts.isEmpty) { @@ -286,20 +285,15 @@ class _FreeUpSpaceSettingsState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text('cleanup_filter_description'.t(context: context), style: subtitleStyle), - const SizedBox(height: 16), + Text('keep_description'.t(context: context), style: subtitleStyle), + const SizedBox(height: 4), SwitchListTile( contentPadding: EdgeInsets.zero, title: Text( 'keep_favorites'.t(context: context), style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5), ), - subtitle: Text( - 'keep_favorites_description'.t(context: context), - style: context.textTheme.bodyMedium!.copyWith( - color: context.textTheme.bodyMedium!.color!.withAlpha(215), - ), - ), + value: state.keepFavorites, onChanged: (value) { ref.read(cleanupProvider.notifier).setKeepFavorites(value); @@ -307,16 +301,16 @@ class _FreeUpSpaceSettingsState extends ConsumerState { }, ), const SizedBox(height: 8), - _ExcludedAlbumsSection( - excludedAlbumIds: state.excludedAlbumIds, + _KeepAlbumsSection( + albumIds: state.keepAlbumIds, onAlbumToggled: (albumId) { - ref.read(cleanupProvider.notifier).toggleExcludedAlbum(albumId); + ref.read(cleanupProvider.notifier).toggleKeepAlbum(albumId); _onKeepSettingsChanged(); }, ), const SizedBox(height: 16), Text( - 'cleanup_keep_all_media_type'.t(context: context), + 'always_keep'.t(context: context), style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5), ), const SizedBox(height: 4), @@ -748,11 +742,11 @@ class _DatePresetCard extends StatelessWidget { } } -class _ExcludedAlbumsSection extends ConsumerWidget { - final Set excludedAlbumIds; +class _KeepAlbumsSection extends ConsumerWidget { + final Set albumIds; final ValueChanged onAlbumToggled; - const _ExcludedAlbumsSection({required this.excludedAlbumIds, required this.onAlbumToggled}); + const _KeepAlbumsSection({required this.albumIds, required this.onAlbumToggled}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -762,15 +756,11 @@ class _ExcludedAlbumsSection extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'exclude_albums'.t(context: context), + 'keep_albums'.t(context: context), style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5), ), - const SizedBox(height: 4), - Text( - 'exclude_albums_description'.t(context: context), - style: context.textTheme.bodyMedium!.copyWith(color: context.textTheme.bodyMedium!.color!.withAlpha(215)), - ), - const SizedBox(height: 12), + + const SizedBox(height: 8), albumsAsync.when( loading: () => const Center( child: Padding(padding: EdgeInsets.all(16.0), child: CircularProgressIndicator(strokeWidth: 2)), @@ -801,22 +791,18 @@ class _ExcludedAlbumsSection extends ConsumerWidget { itemCount: albums.length, itemBuilder: (context, index) { final album = albums[index]; - final isExcluded = excludedAlbumIds.contains(album.id); - return _AlbumExclusionTile( - album: album, - isExcluded: isExcluded, - onToggle: () => onAlbumToggled(album.id), - ); + final isSelected = albumIds.contains(album.id); + return _AlbumTile(album: album, isSelected: isSelected, onToggle: () => onAlbumToggled(album.id)); }, ), ), ); }, ), - if (excludedAlbumIds.isNotEmpty) ...[ + if (albumIds.isNotEmpty) ...[ const SizedBox(height: 8), Text( - 'excluded_albums_count'.t(context: context, args: {'count': excludedAlbumIds.length.toString()}), + 'keep_albums_count'.t(context: context, args: {'count': albumIds.length.toString()}), style: context.textTheme.bodySmall?.copyWith( color: context.colorScheme.primary, fontWeight: FontWeight.w500, @@ -828,12 +814,12 @@ class _ExcludedAlbumsSection extends ConsumerWidget { } } -class _AlbumExclusionTile extends StatelessWidget { +class _AlbumTile extends StatelessWidget { final LocalAlbum album; - final bool isExcluded; + final bool isSelected; final VoidCallback onToggle; - const _AlbumExclusionTile({required this.album, required this.isExcluded, required this.onToggle}); + const _AlbumTile({required this.album, required this.isSelected, required this.onToggle}); @override Widget build(BuildContext context) { @@ -841,13 +827,13 @@ class _AlbumExclusionTile extends StatelessWidget { dense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0), leading: Icon( - isExcluded ? Icons.check_circle : Icons.circle_outlined, - color: isExcluded ? context.colorScheme.primary : context.colorScheme.onSurfaceVariant, + isSelected ? Icons.check_circle : Icons.circle_outlined, + color: isSelected ? context.colorScheme.primary : context.colorScheme.onSurfaceVariant, size: 20, ), title: Text( album.name, - style: context.textTheme.bodyMedium?.copyWith(color: isExcluded ? context.colorScheme.primary : null), + style: context.textTheme.bodyMedium?.copyWith(color: isSelected ? context.colorScheme.primary : null), maxLines: 1, overflow: TextOverflow.ellipsis, ), diff --git a/mobile/test/infrastructure/repositories/local_asset_repository_test.dart b/mobile/test/infrastructure/repositories/local_asset_repository_test.dart index 60300f98be..1a0ebec5c5 100644 --- a/mobile/test/infrastructure/repositories/local_asset_repository_test.dart +++ b/mobile/test/infrastructure/repositories/local_asset_repository_test.dart @@ -462,7 +462,7 @@ void main() { await insertRemoteAsset(id: 'remote-excluded', checksum: 'checksum-excluded', ownerId: userId); await insertLocalAlbumAsset(albumId: 'album-exclude', assetId: 'local-in-excluded'); - final candidates = await repository.getRemovalCandidates(userId, cutoffDate, excludedAlbumIds: {'album-exclude'}); + final candidates = await repository.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {'album-exclude'}); expect(candidates.length, 1); expect(candidates[0].id, 'local-in-included'); @@ -510,7 +510,7 @@ void main() { final candidates = await repository.getRemovalCandidates( userId, cutoffDate, - excludedAlbumIds: {'album-1', 'album-2'}, + keepAlbumIds: {'album-1', 'album-2'}, ); expect(candidates.length, 1); @@ -533,11 +533,7 @@ void main() { await insertLocalAlbumAsset(albumId: 'album-included', assetId: 'local-both'); await insertLocalAlbumAsset(albumId: 'album-excluded', assetId: 'local-both'); - final candidates = await repository.getRemovalCandidates( - userId, - cutoffDate, - excludedAlbumIds: {'album-excluded'}, - ); + final candidates = await repository.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {'album-excluded'}); expect(candidates, isEmpty); }); @@ -565,7 +561,7 @@ void main() { await insertRemoteAsset(id: 'remote-2', checksum: 'checksum-2', ownerId: userId); // Empty excludedAlbumIds should include all eligible assets - final candidates = await repository.getRemovalCandidates(userId, cutoffDate, excludedAlbumIds: {}); + final candidates = await repository.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {}); expect(candidates.length, 2); }); @@ -594,11 +590,7 @@ void main() { await insertRemoteAsset(id: 'remote-in-excluded', checksum: 'checksum-in-excluded', ownerId: userId); await insertLocalAlbumAsset(albumId: 'album-excluded', assetId: 'local-in-excluded'); - final candidates = await repository.getRemovalCandidates( - userId, - cutoffDate, - excludedAlbumIds: {'album-excluded'}, - ); + final candidates = await repository.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {'album-excluded'}); expect(candidates.length, 1); expect(candidates[0].id, 'local-no-album'); @@ -645,7 +637,7 @@ void main() { userId, cutoffDate, keepMediaType: AssetKeepType.photosOnly, - excludedAlbumIds: {'album-excluded'}, + keepAlbumIds: {'album-excluded'}, ); expect(candidates.length, 1);