mirror of
https://github.com/immich-app/immich.git
synced 2026-01-23 01:49:01 -08:00
Compare commits
1 Commits
main
...
exclude-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a644cc0a3c |
@@ -1007,6 +1007,7 @@
|
||||
"error_change_sort_album": "Failed to change album sort order",
|
||||
"error_delete_face": "Error deleting face from asset",
|
||||
"error_getting_places": "Error getting places",
|
||||
"error_loading_albums": "Error loading albums",
|
||||
"error_loading_image": "Error loading image",
|
||||
"error_loading_partners": "Error loading partners: {error}",
|
||||
"error_retrieving_asset_information": "Error retrieving asset information",
|
||||
@@ -1138,6 +1139,9 @@
|
||||
"unable_to_upload_file": "Unable to upload file"
|
||||
},
|
||||
"errors_text": "Errors",
|
||||
"exclude_albums": "Exclude albums",
|
||||
"exclude_albums_description": "Assets in selected albums will not be removed from your device",
|
||||
"excluded_albums_count": "{count} albums excluded",
|
||||
"exclusion_pattern": "Exclusion pattern",
|
||||
"exif": "Exif",
|
||||
"exif_bottom_sheet_description": "Add Description...",
|
||||
@@ -1553,6 +1557,7 @@
|
||||
"next_memory": "Next memory",
|
||||
"no": "No",
|
||||
"no_actions_added": "No actions added yet",
|
||||
"no_albums_found": "No albums found",
|
||||
"no_albums_message": "Create an album to organize your photos and videos",
|
||||
"no_albums_with_name_yet": "It looks like you do not have any albums with this name yet.",
|
||||
"no_albums_yet": "It looks like you do not have any albums yet.",
|
||||
|
||||
@@ -135,6 +135,7 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
||||
DateTime cutoffDate, {
|
||||
AssetFilterType filterType = AssetFilterType.all,
|
||||
bool keepFavorites = true,
|
||||
Set<String> excludedAlbumIds = const {},
|
||||
}) async {
|
||||
final iosSharedAlbumAssets = _db.localAlbumAssetEntity.selectOnly()
|
||||
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||
@@ -159,6 +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()
|
||||
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||
..where(_db.localAlbumAssetEntity.albumId.isIn(excludedAlbumIds));
|
||||
whereClause = whereClause & _db.localAssetEntity.id.isNotInQuery(excludedAlbumAssets);
|
||||
}
|
||||
|
||||
if (filterType == AssetFilterType.photosOnly) {
|
||||
whereClause = whereClause & _db.localAssetEntity.type.equalsValue(AssetType.image);
|
||||
} else if (filterType == AssetFilterType.videosOnly) {
|
||||
|
||||
@@ -11,6 +11,7 @@ class CleanupState {
|
||||
final bool isDeleting;
|
||||
final AssetFilterType filterType;
|
||||
final bool keepFavorites;
|
||||
final Set<String> excludedAlbumIds;
|
||||
|
||||
const CleanupState({
|
||||
this.selectedDate,
|
||||
@@ -19,6 +20,7 @@ class CleanupState {
|
||||
this.isDeleting = false,
|
||||
this.filterType = AssetFilterType.all,
|
||||
this.keepFavorites = true,
|
||||
this.excludedAlbumIds = const {},
|
||||
});
|
||||
|
||||
CleanupState copyWith({
|
||||
@@ -28,6 +30,7 @@ class CleanupState {
|
||||
bool? isDeleting,
|
||||
AssetFilterType? filterType,
|
||||
bool? keepFavorites,
|
||||
Set<String>? excludedAlbumIds,
|
||||
}) {
|
||||
return CleanupState(
|
||||
selectedDate: selectedDate ?? this.selectedDate,
|
||||
@@ -36,6 +39,7 @@ class CleanupState {
|
||||
isDeleting: isDeleting ?? this.isDeleting,
|
||||
filterType: filterType ?? this.filterType,
|
||||
keepFavorites: keepFavorites ?? this.keepFavorites,
|
||||
excludedAlbumIds: excludedAlbumIds ?? this.excludedAlbumIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -62,6 +66,20 @@ class CleanupNotifier extends StateNotifier<CleanupState> {
|
||||
state = state.copyWith(keepFavorites: keepFavorites, assetsToDelete: []);
|
||||
}
|
||||
|
||||
void toggleExcludedAlbum(String albumId) {
|
||||
final newExcludedAlbumIds = Set<String>.from(state.excludedAlbumIds);
|
||||
if (newExcludedAlbumIds.contains(albumId)) {
|
||||
newExcludedAlbumIds.remove(albumId);
|
||||
} else {
|
||||
newExcludedAlbumIds.add(albumId);
|
||||
}
|
||||
state = state.copyWith(excludedAlbumIds: newExcludedAlbumIds, assetsToDelete: []);
|
||||
}
|
||||
|
||||
void setExcludedAlbumIds(Set<String> albumIds) {
|
||||
state = state.copyWith(excludedAlbumIds: albumIds, assetsToDelete: []);
|
||||
}
|
||||
|
||||
Future<void> scanAssets() async {
|
||||
if (_userId == null || state.selectedDate == null) {
|
||||
return;
|
||||
@@ -74,6 +92,7 @@ class CleanupNotifier extends StateNotifier<CleanupState> {
|
||||
state.selectedDate!,
|
||||
filterType: state.filterType,
|
||||
keepFavorites: state.keepFavorites,
|
||||
excludedAlbumIds: state.excludedAlbumIds,
|
||||
);
|
||||
state = state.copyWith(assetsToDelete: assets, isScanning: false);
|
||||
} catch (e) {
|
||||
|
||||
@@ -20,12 +20,14 @@ class CleanupService {
|
||||
DateTime cutoffDate, {
|
||||
AssetFilterType filterType = AssetFilterType.all,
|
||||
bool keepFavorites = true,
|
||||
Set<String> excludedAlbumIds = const {},
|
||||
}) {
|
||||
return _localAssetRepository.getRemovalCandidates(
|
||||
userId,
|
||||
cutoffDate,
|
||||
filterType: filterType,
|
||||
keepFavorites: keepFavorites,
|
||||
excludedAlbumIds: excludedAlbumIds,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,14 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/providers/cleanup.provider.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class FreeUpSpaceSettings extends ConsumerStatefulWidget {
|
||||
@@ -195,6 +197,11 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
|
||||
if (state.keepFavorites) {
|
||||
parts.add('keep_favorites'.t(context: context));
|
||||
}
|
||||
if (state.excludedAlbumIds.isNotEmpty) {
|
||||
parts.add(
|
||||
'excluded_albums_count'.t(context: context, args: {'count': state.excludedAlbumIds.length.toString()}),
|
||||
);
|
||||
}
|
||||
return parts.join(' • ');
|
||||
}
|
||||
|
||||
@@ -397,6 +404,14 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_ExcludedAlbumsSection(
|
||||
excludedAlbumIds: state.excludedAlbumIds,
|
||||
onAlbumToggled: (albumId) {
|
||||
ref.read(cleanupProvider.notifier).toggleExcludedAlbum(albumId);
|
||||
setState(() => _hasScanned = false);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _goToScanStep,
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
@@ -701,3 +716,112 @@ class _DatePresetCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExcludedAlbumsSection extends ConsumerWidget {
|
||||
final Set<String> excludedAlbumIds;
|
||||
final ValueChanged<String> onAlbumToggled;
|
||||
|
||||
const _ExcludedAlbumsSection({required this.excludedAlbumIds, required this.onAlbumToggled});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final albumsAsync = ref.watch(localAlbumProvider);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'exclude_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),
|
||||
albumsAsync.when(
|
||||
loading: () => const Center(
|
||||
child: Padding(padding: EdgeInsets.all(16.0), child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
),
|
||||
error: (error, stack) => Text(
|
||||
'error_loading_albums'.t(context: context),
|
||||
style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.error),
|
||||
),
|
||||
data: (albums) {
|
||||
if (albums.isEmpty) {
|
||||
return Text(
|
||||
'no_albums_found'.t(context: context),
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: context.colorScheme.outlineVariant),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (excludedAlbumIds.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'excluded_albums_count'.t(context: context, args: {'count': excludedAlbumIds.length.toString()}),
|
||||
style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.error, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumExclusionTile extends StatelessWidget {
|
||||
final LocalAlbum album;
|
||||
final bool isExcluded;
|
||||
final VoidCallback onToggle;
|
||||
|
||||
const _AlbumExclusionTile({required this.album, required this.isExcluded, required this.onToggle});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0),
|
||||
leading: Icon(
|
||||
isExcluded ? Icons.remove_circle : Icons.photo_album_outlined,
|
||||
color: isExcluded ? context.colorScheme.error : context.colorScheme.onSurfaceVariant,
|
||||
size: 20,
|
||||
),
|
||||
title: Text(
|
||||
album.name,
|
||||
style: context.textTheme.bodyMedium?.copyWith(color: isExcluded ? context.colorScheme.error : null),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Text(
|
||||
album.assetCount.toString(),
|
||||
style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant),
|
||||
),
|
||||
onTap: onToggle,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,5 +434,222 @@ void main() {
|
||||
|
||||
expect(candidates, isEmpty);
|
||||
});
|
||||
|
||||
test('excludes assets in user-excluded albums', () async {
|
||||
// Create two regular albums
|
||||
await insertLocalAlbum(id: 'album-include', name: 'Include Album', isIosSharedAlbum: false);
|
||||
await insertLocalAlbum(id: 'album-exclude', name: 'Exclude Album', isIosSharedAlbum: false);
|
||||
|
||||
// Asset in included album - should be included
|
||||
await insertLocalAsset(
|
||||
id: 'local-in-included',
|
||||
checksum: 'checksum-included',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-included', checksum: 'checksum-included', ownerId: userId);
|
||||
await insertLocalAlbumAsset(albumId: 'album-include', assetId: 'local-in-included');
|
||||
|
||||
// Asset in excluded album - should NOT be included
|
||||
await insertLocalAsset(
|
||||
id: 'local-in-excluded',
|
||||
checksum: 'checksum-excluded',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
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'});
|
||||
|
||||
expect(candidates.length, 1);
|
||||
expect(candidates[0].id, 'local-in-included');
|
||||
});
|
||||
|
||||
test('excludes assets that are in any of multiple excluded albums', () async {
|
||||
// Create multiple albums
|
||||
await insertLocalAlbum(id: 'album-1', name: 'Album 1', isIosSharedAlbum: false);
|
||||
await insertLocalAlbum(id: 'album-2', name: 'Album 2', isIosSharedAlbum: false);
|
||||
await insertLocalAlbum(id: 'album-3', name: 'Album 3', isIosSharedAlbum: false);
|
||||
|
||||
// Asset in album-1 (excluded) - should NOT be included
|
||||
await insertLocalAsset(
|
||||
id: 'local-1',
|
||||
checksum: 'checksum-1',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-1', checksum: 'checksum-1', ownerId: userId);
|
||||
await insertLocalAlbumAsset(albumId: 'album-1', assetId: 'local-1');
|
||||
|
||||
// Asset in album-2 (excluded) - should NOT be included
|
||||
await insertLocalAsset(
|
||||
id: 'local-2',
|
||||
checksum: 'checksum-2',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-2', checksum: 'checksum-2', ownerId: userId);
|
||||
await insertLocalAlbumAsset(albumId: 'album-2', assetId: 'local-2');
|
||||
|
||||
// Asset in album-3 (not excluded) - should be included
|
||||
await insertLocalAsset(
|
||||
id: 'local-3',
|
||||
checksum: 'checksum-3',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-3', checksum: 'checksum-3', ownerId: userId);
|
||||
await insertLocalAlbumAsset(albumId: 'album-3', assetId: 'local-3');
|
||||
|
||||
final candidates = await repository.getRemovalCandidates(
|
||||
userId,
|
||||
cutoffDate,
|
||||
excludedAlbumIds: {'album-1', 'album-2'},
|
||||
);
|
||||
|
||||
expect(candidates.length, 1);
|
||||
expect(candidates[0].id, 'local-3');
|
||||
});
|
||||
|
||||
test('excludes asset that is in both excluded and non-excluded album', () async {
|
||||
await insertLocalAlbum(id: 'album-included', name: 'Included Album', isIosSharedAlbum: false);
|
||||
await insertLocalAlbum(id: 'album-excluded', name: 'Excluded Album', isIosSharedAlbum: false);
|
||||
|
||||
// Asset in BOTH albums - should be excluded because it's in an excluded album
|
||||
await insertLocalAsset(
|
||||
id: 'local-both',
|
||||
checksum: 'checksum-both',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-both', checksum: 'checksum-both', ownerId: userId);
|
||||
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'},
|
||||
);
|
||||
|
||||
expect(candidates, isEmpty);
|
||||
});
|
||||
|
||||
test('includes all assets when excludedAlbumIds is empty', () async {
|
||||
await insertLocalAlbum(id: 'album-1', name: 'Album 1', isIosSharedAlbum: false);
|
||||
|
||||
await insertLocalAsset(
|
||||
id: 'local-1',
|
||||
checksum: 'checksum-1',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-1', checksum: 'checksum-1', ownerId: userId);
|
||||
await insertLocalAlbumAsset(albumId: 'album-1', assetId: 'local-1');
|
||||
|
||||
await insertLocalAsset(
|
||||
id: 'local-2',
|
||||
checksum: 'checksum-2',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
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: {});
|
||||
|
||||
expect(candidates.length, 2);
|
||||
});
|
||||
|
||||
test('excludes asset not in any album when album is excluded', () async {
|
||||
await insertLocalAlbum(id: 'album-excluded', name: 'Excluded Album', isIosSharedAlbum: false);
|
||||
|
||||
// Asset NOT in any album - should be included
|
||||
await insertLocalAsset(
|
||||
id: 'local-no-album',
|
||||
checksum: 'checksum-no-album',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-no-album', checksum: 'checksum-no-album', ownerId: userId);
|
||||
|
||||
// Asset in excluded album - should NOT be included
|
||||
await insertLocalAsset(
|
||||
id: 'local-in-excluded',
|
||||
checksum: 'checksum-in-excluded',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
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'},
|
||||
);
|
||||
|
||||
expect(candidates.length, 1);
|
||||
expect(candidates[0].id, 'local-no-album');
|
||||
});
|
||||
|
||||
test('combines excludedAlbumIds with other filters correctly', () async {
|
||||
await insertLocalAlbum(id: 'album-excluded', name: 'Excluded Album', isIosSharedAlbum: false);
|
||||
await insertLocalAlbum(id: 'album-regular', name: 'Regular Album', isIosSharedAlbum: false);
|
||||
|
||||
// Photo in excluded album - should NOT be included
|
||||
await insertLocalAsset(
|
||||
id: 'local-photo-excluded',
|
||||
checksum: 'checksum-photo-excluded',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-photo-excluded', checksum: 'checksum-photo-excluded', ownerId: userId);
|
||||
await insertLocalAlbumAsset(albumId: 'album-excluded', assetId: 'local-photo-excluded');
|
||||
|
||||
// Video in regular album - should NOT be included (filtering photos only)
|
||||
await insertLocalAsset(
|
||||
id: 'local-video',
|
||||
checksum: 'checksum-video',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.video,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-video', checksum: 'checksum-video', ownerId: userId);
|
||||
await insertLocalAlbumAsset(albumId: 'album-regular', assetId: 'local-video');
|
||||
|
||||
// Photo in regular album - should be included
|
||||
await insertLocalAsset(
|
||||
id: 'local-photo-regular',
|
||||
checksum: 'checksum-photo-regular',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-photo-regular', checksum: 'checksum-photo-regular', ownerId: userId);
|
||||
await insertLocalAlbumAsset(albumId: 'album-regular', assetId: 'local-photo-regular');
|
||||
|
||||
final candidates = await repository.getRemovalCandidates(
|
||||
userId,
|
||||
cutoffDate,
|
||||
filterType: AssetFilterType.photosOnly,
|
||||
excludedAlbumIds: {'album-excluded'},
|
||||
);
|
||||
|
||||
expect(candidates.length, 1);
|
||||
expect(candidates[0].id, 'local-photo-regular');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user