diff --git a/mobile/lib/entities/backup_album.entity.dart b/mobile/lib/entities/backup_album.entity.dart
index 5229d93782..4d4d7b3aa3 100644
--- a/mobile/lib/entities/backup_album.entity.dart
+++ b/mobile/lib/entities/backup_album.entity.dart
@@ -18,4 +18,5 @@ class BackupAlbum {
enum BackupSelection {
none,
select,
+ exclude;
}
diff --git a/mobile/lib/entities/backup_album.entity.g.dart b/mobile/lib/entities/backup_album.entity.g.dart
index aad62aed74..7fb6c0e03b 100644
--- a/mobile/lib/entities/backup_album.entity.g.dart
+++ b/mobile/lib/entities/backup_album.entity.g.dart
@@ -107,10 +107,12 @@ P _backupAlbumDeserializeProp
(
const _BackupAlbumselectionEnumValueMap = {
'none': 0,
'select': 1,
+ 'exclude': 2,
};
const _BackupAlbumselectionValueEnumMap = {
0: BackupSelection.none,
1: BackupSelection.select,
+ 2: BackupSelection.exclude,
};
Id _backupAlbumGetId(BackupAlbum object) {
diff --git a/mobile/lib/models/backup/backup_state.model.dart b/mobile/lib/models/backup/backup_state.model.dart
index b9fcefc79d..bb693a5b75 100644
--- a/mobile/lib/models/backup/backup_state.model.dart
+++ b/mobile/lib/models/backup/backup_state.model.dart
@@ -38,9 +38,10 @@ class BackUpState {
/// All available albums on the device
final List availableAlbums;
final Set selectedBackupAlbums;
+ final Set excludedBackupAlbums;
/// Assets that are not overlapping in selected backup albums and excluded backup albums
- final Set backupCandidates;
+ final Set allUniqueAssets;
/// All assets from the selected albums that have been backup
final Set selectedAlbumsBackupAssetsIds;
@@ -67,7 +68,8 @@ class BackUpState {
required this.backupTriggerDelay,
required this.availableAlbums,
required this.selectedBackupAlbums,
- required this.backupCandidates,
+ required this.excludedBackupAlbums,
+ required this.allUniqueAssets,
required this.selectedAlbumsBackupAssetsIds,
required this.currentUploadAsset,
});
@@ -91,7 +93,8 @@ class BackUpState {
int? backupTriggerDelay,
List? availableAlbums,
Set? selectedBackupAlbums,
- Set? backupCandidates,
+ Set? excludedBackupAlbums,
+ Set? allUniqueAssets,
Set? selectedAlbumsBackupAssetsIds,
CurrentUploadAsset? currentUploadAsset,
}) {
@@ -118,7 +121,8 @@ class BackUpState {
backupTriggerDelay: backupTriggerDelay ?? this.backupTriggerDelay,
availableAlbums: availableAlbums ?? this.availableAlbums,
selectedBackupAlbums: selectedBackupAlbums ?? this.selectedBackupAlbums,
- backupCandidates: backupCandidates ?? this.backupCandidates,
+ excludedBackupAlbums: excludedBackupAlbums ?? this.excludedBackupAlbums,
+ allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets,
selectedAlbumsBackupAssetsIds:
selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds,
currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset,
@@ -127,7 +131,7 @@ class BackUpState {
@override
String toString() {
- return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, progressInFileSize: $progressInFileSize, progressInFileSpeed: $progressInFileSpeed, progressInFileSpeeds: $progressInFileSpeeds, progressInFileSpeedUpdateTime: $progressInFileSpeedUpdateTime, progressInFileSpeedUpdateSentBytes: $progressInFileSpeedUpdateSentBytes, iCloudDownloadProgress: $iCloudDownloadProgress, cancelToken: $cancelToken, serverInfo: $serverInfo, autoBackup: $autoBackup, backgroundBackup: $backgroundBackup, backupRequireWifi: $backupRequireWifi, backupRequireCharging: $backupRequireCharging, backupTriggerDelay: $backupTriggerDelay, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, backupCandidates: $backupCandidates, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)';
+ return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, progressInFileSize: $progressInFileSize, progressInFileSpeed: $progressInFileSpeed, progressInFileSpeeds: $progressInFileSpeeds, progressInFileSpeedUpdateTime: $progressInFileSpeedUpdateTime, progressInFileSpeedUpdateSentBytes: $progressInFileSpeedUpdateSentBytes, iCloudDownloadProgress: $iCloudDownloadProgress, cancelToken: $cancelToken, serverInfo: $serverInfo, autoBackup: $autoBackup, backgroundBackup: $backgroundBackup, backupRequireWifi: $backupRequireWifi, backupRequireCharging: $backupRequireCharging, backupTriggerDelay: $backupTriggerDelay, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)';
}
@override
@@ -154,7 +158,8 @@ class BackUpState {
other.backupTriggerDelay == backupTriggerDelay &&
collectionEquals(other.availableAlbums, availableAlbums) &&
collectionEquals(other.selectedBackupAlbums, selectedBackupAlbums) &&
- collectionEquals(other.backupCandidates, backupCandidates) &&
+ collectionEquals(other.excludedBackupAlbums, excludedBackupAlbums) &&
+ collectionEquals(other.allUniqueAssets, allUniqueAssets) &&
collectionEquals(
other.selectedAlbumsBackupAssetsIds,
selectedAlbumsBackupAssetsIds,
@@ -182,7 +187,8 @@ class BackUpState {
backupTriggerDelay.hashCode ^
availableAlbums.hashCode ^
selectedBackupAlbums.hashCode ^
- backupCandidates.hashCode ^
+ excludedBackupAlbums.hashCode ^
+ allUniqueAssets.hashCode ^
selectedAlbumsBackupAssetsIds.hashCode ^
currentUploadAsset.hashCode;
}
diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart
index 70dc0dbf53..ecfebd3cb7 100644
--- a/mobile/lib/pages/backup/backup_album_selection.page.dart
+++ b/mobile/lib/pages/backup/backup_album_selection.page.dart
@@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/widgets/backup/album_info_card.dart';
@@ -16,6 +17,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// final availableAlbums = ref.watch(backupProvider).availableAlbums;
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
+ final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
final isDarkTheme = context.isDarkTheme;
final albums = ref.watch(backupProvider).availableAlbums;
@@ -109,6 +111,83 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
}).toSet();
}
+ buildExcludedAlbumNameChip() {
+ return excludedBackupAlbums.map((album) {
+ void removeSelection() {
+ ref
+ .watch(backupProvider.notifier)
+ .removeExcludedAlbumForBackup(album);
+ }
+
+ return GestureDetector(
+ onTap: removeSelection,
+ child: Padding(
+ padding: const EdgeInsets.only(right: 8.0),
+ child: Chip(
+ label: Text(
+ album.name,
+ style: TextStyle(
+ fontSize: 12,
+ color: isDarkTheme ? Colors.black : immichBackgroundColor,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ backgroundColor: Colors.red[300],
+ deleteIconColor:
+ isDarkTheme ? Colors.black : immichBackgroundColor,
+ deleteIcon: const Icon(
+ Icons.cancel_rounded,
+ size: 15,
+ ),
+ onDeleted: removeSelection,
+ ),
+ ),
+ );
+ }).toSet();
+ }
+
+ // buildSearchBar() {
+ // return Padding(
+ // padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 8.0),
+ // child: TextFormField(
+ // onChanged: (searchValue) {
+ // // if (searchValue.isEmpty) {
+ // // albums = availableAlbums;
+ // // } else {
+ // // albums.value = availableAlbums
+ // // .where(
+ // // (album) => album.name
+ // // .toLowerCase()
+ // // .contains(searchValue.toLowerCase()),
+ // // )
+ // // .toList();
+ // // }
+ // },
+ // decoration: InputDecoration(
+ // contentPadding: const EdgeInsets.symmetric(
+ // horizontal: 8.0,
+ // vertical: 8.0,
+ // ),
+ // hintText: "Search",
+ // hintStyle: TextStyle(
+ // color: isDarkTheme ? Colors.white : Colors.grey,
+ // fontSize: 14.0,
+ // ),
+ // prefixIcon: const Icon(
+ // Icons.search,
+ // color: Colors.grey,
+ // ),
+ // border: OutlineInputBorder(
+ // borderRadius: BorderRadius.circular(10),
+ // borderSide: BorderSide.none,
+ // ),
+ // filled: true,
+ // fillColor: isDarkTheme ? Colors.white30 : Colors.grey[200],
+ // ),
+ // ),
+ // );
+ // }
+
return Scaffold(
appBar: AppBar(
leading: IconButton(
@@ -144,6 +223,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
child: Wrap(
children: [
...buildSelectedAlbumNameChip(),
+ ...buildExcludedAlbumNameChip(),
],
),
),
diff --git a/mobile/lib/pages/backup/backup_controller.page.dart b/mobile/lib/pages/backup/backup_controller.page.dart
index 847b2518b6..89384cf97a 100644
--- a/mobile/lib/pages/backup/backup_controller.page.dart
+++ b/mobile/lib/pages/backup/backup_controller.page.dart
@@ -29,7 +29,7 @@ class BackupControllerPage extends HookConsumerWidget {
final didGetBackupInfo = useState(false);
bool hasExclusiveAccess =
backupState.backupProgress != BackUpProgressEnum.inBackground;
- bool shouldBackup = backupState.backupCandidates.length -
+ bool shouldBackup = backupState.allUniqueAssets.length -
backupState.selectedAlbumsBackupAssetsIds.length ==
0 ||
!hasExclusiveAccess
@@ -100,6 +100,29 @@ class BackupControllerPage extends HookConsumerWidget {
}
}
+ Widget buildExcludedAlbumName() {
+ var text = "backup_controller_page_excluded".tr();
+ var albums = ref.watch(backupProvider).excludedBackupAlbums;
+
+ if (albums.isNotEmpty) {
+ for (var album in albums) {
+ text += "${album.name}, ";
+ }
+
+ return Padding(
+ padding: const EdgeInsets.only(top: 8.0),
+ child: Text(
+ text.trim().substring(0, text.length - 2),
+ style: context.textTheme.labelLarge?.copyWith(
+ color: Colors.red[300],
+ ),
+ ),
+ );
+ } else {
+ return const SizedBox();
+ }
+ }
+
buildFolderSelectionTile() {
return Padding(
padding: const EdgeInsets.only(top: 8.0),
@@ -131,6 +154,7 @@ class BackupControllerPage extends HookConsumerWidget {
style: context.textTheme.bodyMedium,
).tr(),
buildSelectedAlbumName(),
+ buildExcludedAlbumName(),
],
),
),
@@ -268,7 +292,7 @@ class BackupControllerPage extends HookConsumerWidget {
subtitle: "backup_controller_page_total_sub".tr(),
info: ref.watch(backupProvider).availableAlbums.isEmpty
? "..."
- : "${backupState.backupCandidates.length}",
+ : "${backupState.allUniqueAssets.length}",
),
BackupInfoCard(
title: "backup_controller_page_backup".tr(),
@@ -282,7 +306,7 @@ class BackupControllerPage extends HookConsumerWidget {
subtitle: "backup_controller_page_remainder_sub".tr(),
info: ref.watch(backupProvider).availableAlbums.isEmpty
? "..."
- : "${max(0, backupState.backupCandidates.length - backupState.selectedAlbumsBackupAssetsIds.length)}",
+ : "${max(0, backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length)}",
),
const Divider(),
const CurrentUploadingAssetInfoBox(),
diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart
index 301b50aef6..58027e3b94 100644
--- a/mobile/lib/providers/backup/backup.provider.dart
+++ b/mobile/lib/providers/backup/backup.provider.dart
@@ -61,7 +61,8 @@ class BackupNotifier extends StateNotifier {
),
availableAlbums: const [],
selectedBackupAlbums: const {},
- backupCandidates: const {},
+ excludedBackupAlbums: const {},
+ allUniqueAssets: const {},
selectedAlbumsBackupAssetsIds: const {},
currentUploadAsset: CurrentUploadAsset(
id: '...',
@@ -93,10 +94,22 @@ class BackupNotifier extends StateNotifier {
/// The total unique assets will be used for backing mechanism
///
void addAlbumForBackup(AvailableAlbum album) {
+ if (state.excludedBackupAlbums.contains(album)) {
+ removeExcludedAlbumForBackup(album);
+ }
+
state = state
.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album});
}
+ void addExcludedAlbumForBackup(AvailableAlbum album) {
+ if (state.selectedBackupAlbums.contains(album)) {
+ removeAlbumForBackup(album);
+ }
+ state = state
+ .copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album});
+ }
+
void removeAlbumForBackup(AvailableAlbum album) {
Set currentSelectedAlbums = state.selectedBackupAlbums;
@@ -105,6 +118,14 @@ class BackupNotifier extends StateNotifier {
state = state.copyWith(selectedBackupAlbums: currentSelectedAlbums);
}
+ void removeExcludedAlbumForBackup(AvailableAlbum album) {
+ Set currentExcludedAlbums = state.excludedBackupAlbums;
+
+ currentExcludedAlbums.removeWhere((a) => a == album);
+
+ state = state.copyWith(excludedBackupAlbums: currentExcludedAlbums);
+ }
+
Future backupAlbumSelectionDone() {
if (state.selectedBackupAlbums.isEmpty) {
// disable any backup
@@ -219,6 +240,8 @@ class BackupNotifier extends StateNotifier {
}
state = state.copyWith(availableAlbums: availableAlbums);
+ final List excludedBackupAlbums =
+ await _backupService.excludedAlbumsQuery().findAll();
final List selectedBackupAlbums =
await _backupService.selectedAlbumsQuery().findAll();
@@ -236,8 +259,22 @@ class BackupNotifier extends StateNotifier {
}
}
+ final Set excludedAlbums = {};
+ for (final BackupAlbum ba in excludedBackupAlbums) {
+ final albumAsset = albumMap[ba.id];
+
+ if (albumAsset != null) {
+ excludedAlbums.add(
+ AvailableAlbum(albumEntity: albumAsset, lastBackup: ba.lastBackup),
+ );
+ } else {
+ log.severe('Excluded album not found');
+ }
+ }
+
state = state.copyWith(
selectedBackupAlbums: selectedAlbums,
+ excludedBackupAlbums: excludedAlbums,
);
log.info(
@@ -253,7 +290,8 @@ class BackupNotifier extends StateNotifier {
///
Future _updateBackupAssetCount() async {
final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds();
- final Set backupCandidates = {};
+ final Set assetsFromSelectedAlbums = {};
+ final Set assetsFromExcludedAlbums = {};
for (final album in state.selectedBackupAlbums) {
final assetCount = await album.albumEntity.assetCountAsync;
@@ -266,9 +304,25 @@ class BackupNotifier extends StateNotifier {
start: 0,
end: assetCount,
);
- backupCandidates.addAll(assets);
+ assetsFromSelectedAlbums.addAll(assets);
}
+ for (final album in state.excludedBackupAlbums) {
+ final assetCount = await album.albumEntity.assetCountAsync;
+
+ if (assetCount == 0) {
+ continue;
+ }
+
+ final assets = await album.albumEntity.getAssetListRange(
+ start: 0,
+ end: assetCount,
+ );
+ assetsFromExcludedAlbums.addAll(assets);
+ }
+
+ final Set allUniqueAssets =
+ assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
final allAssetsInDatabase = await _backupService.getDeviceBackupAsset();
if (allAssetsInDatabase == null) {
@@ -277,28 +331,28 @@ class BackupNotifier extends StateNotifier {
// Find asset that were backup from selected albums
final Set selectedAlbumsBackupAssets =
- Set.from(backupCandidates.map((e) => e.id));
+ Set.from(allUniqueAssets.map((e) => e.id));
selectedAlbumsBackupAssets
.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
// Remove duplicated asset from all unique assets
- backupCandidates.removeWhere(
+ allUniqueAssets.removeWhere(
(asset) => duplicatedAssetIds.contains(asset.id),
);
- if (backupCandidates.isEmpty) {
+ if (allUniqueAssets.isEmpty) {
log.info("No assets are selected for back up");
state = state.copyWith(
backupProgress: BackUpProgressEnum.idle,
allAssetsInDatabase: allAssetsInDatabase,
- backupCandidates: {},
+ allUniqueAssets: {},
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
);
} else {
state = state.copyWith(
allAssetsInDatabase: allAssetsInDatabase,
- backupCandidates: backupCandidates,
+ allUniqueAssets: allUniqueAssets,
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
);
}
@@ -333,8 +387,10 @@ class BackupNotifier extends StateNotifier {
final selected = state.selectedBackupAlbums.map(
(e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.select),
);
-
- final backupAlbums = selected.toList();
+ final excluded = state.excludedBackupAlbums.map(
+ (e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.exclude),
+ );
+ final backupAlbums = selected.followedBy(excluded).toList();
backupAlbums.sortBy((e) => e.id);
return _db.writeTxn(() async {
final dbAlbums = await _db.backupAlbums.where().sortById().findAll();
@@ -371,13 +427,13 @@ class BackupNotifier extends StateNotifier {
if (hasPermission) {
await PhotoManager.clearFileCache();
- if (state.backupCandidates.isEmpty) {
+ if (state.allUniqueAssets.isEmpty) {
log.info("No Asset On Device - Abort Backup Process");
state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
return;
}
- Set assetsWillBeBackup = Set.from(state.backupCandidates);
+ Set assetsWillBeBackup = Set.from(state.allUniqueAssets);
// Remove item that has already been backed up
for (final assetId in state.allAssetsInDatabase) {
assetsWillBeBackup.removeWhere((e) => e.id == assetId);
@@ -448,7 +504,7 @@ class BackupNotifier extends StateNotifier {
) {
if (isDuplicated) {
state = state.copyWith(
- backupCandidates: state.backupCandidates
+ allUniqueAssets: state.allUniqueAssets
.where((asset) => asset.id != deviceAssetId)
.toSet(),
);
@@ -462,17 +518,20 @@ class BackupNotifier extends StateNotifier {
);
}
- if (state.backupCandidates.length -
+ if (state.allUniqueAssets.length -
state.selectedAlbumsBackupAssetsIds.length ==
0) {
final latestAssetBackup =
- state.backupCandidates.map((e) => e.modifiedDateTime).reduce(
+ state.allUniqueAssets.map((e) => e.modifiedDateTime).reduce(
(v, e) => e.isAfter(v) ? e : v,
);
state = state.copyWith(
selectedBackupAlbums: state.selectedBackupAlbums
.map((e) => e.copyWith(lastBackup: latestAssetBackup))
.toSet(),
+ excludedBackupAlbums: state.excludedBackupAlbums
+ .map((e) => e.copyWith(lastBackup: latestAssetBackup))
+ .toSet(),
backupProgress: BackUpProgressEnum.done,
progressInPercentage: 0.0,
progressInFileSize: "0 B / 0 B",
@@ -571,8 +630,12 @@ class BackupNotifier extends StateNotifier {
.filter()
.selectionEqualTo(BackupSelection.select)
.findAll();
-
+ final List excludedBackupAlbums = await _db.backupAlbums
+ .filter()
+ .selectionEqualTo(BackupSelection.exclude)
+ .findAll();
Set selectedAlbums = state.selectedBackupAlbums;
+ Set excludedAlbums = state.excludedBackupAlbums;
if (selectedAlbums.isNotEmpty) {
selectedAlbums = _updateAlbumsBackupTime(
selectedAlbums,
@@ -580,10 +643,17 @@ class BackupNotifier extends StateNotifier {
);
}
+ if (excludedAlbums.isNotEmpty) {
+ excludedAlbums = _updateAlbumsBackupTime(
+ excludedAlbums,
+ excludedBackupAlbums,
+ );
+ }
final BackUpProgressEnum previous = state.backupProgress;
state = state.copyWith(
backupProgress: BackUpProgressEnum.inBackground,
selectedBackupAlbums: selectedAlbums,
+ excludedBackupAlbums: excludedAlbums,
);
// assumes the background service is currently running
// if true, waits until it has stopped to start the backup
diff --git a/mobile/lib/providers/backup/backup_verification.provider.dart b/mobile/lib/providers/backup/backup_verification.provider.dart
index bd2c5d5f9d..894b807ec8 100644
--- a/mobile/lib/providers/backup/backup_verification.provider.dart
+++ b/mobile/lib/providers/backup/backup_verification.provider.dart
@@ -23,7 +23,7 @@ class BackupVerification extends _$BackupVerification {
state = true;
final backupState = ref.read(backupProvider);
- if (backupState.backupCandidates.length >
+ if (backupState.allUniqueAssets.length >
backupState.selectedAlbumsBackupAssetsIds.length) {
if (context.mounted) {
ImmichToast.show(
diff --git a/mobile/lib/providers/backup/backup_verification.provider.g.dart b/mobile/lib/providers/backup/backup_verification.provider.g.dart
index aa00550d3a..f222c9bd83 100644
--- a/mobile/lib/providers/backup/backup_verification.provider.g.dart
+++ b/mobile/lib/providers/backup/backup_verification.provider.g.dart
@@ -7,7 +7,7 @@ part of 'backup_verification.provider.dart';
// **************************************************************************
String _$backupVerificationHash() =>
- r'4f64459d68d20de4a61160ec8e9be347ec945fb6';
+ r'b691e0cc27856eef189258d3c102cc73ce4812a4';
/// See also [BackupVerification].
@ProviderFor(BackupVerification)
diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart
index a2a2abae8d..ba8f5c01ed 100644
--- a/mobile/lib/services/background.service.dart
+++ b/mobile/lib/services/background.service.dart
@@ -349,6 +349,7 @@ class BackgroundService {
AppSettingsService settingsService = AppSettingsService();
final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();
+ final excludedAlbums = backupService.excludedAlbumsQuery().findAllSync();
if (selectedAlbums.isEmpty) {
return true;
}
@@ -360,10 +361,11 @@ class BackgroundService {
backupService,
settingsService,
selectedAlbums,
+ excludedAlbums,
);
if (backupOk) {
await Store.delete(StoreKey.backupFailedSince);
- final backupAlbums = [...selectedAlbums];
+ final backupAlbums = [...selectedAlbums, ...excludedAlbums];
backupAlbums.sortBy((e) => e.id);
db.writeTxnSync(() {
final dbAlbums = db.backupAlbums.where().sortById().findAllSync();
@@ -402,6 +404,7 @@ class BackgroundService {
BackupService backupService,
AppSettingsService settingsService,
List selectedAlbums,
+ List excludedAlbums,
) async {
_errorGracePeriodExceeded = _isErrorGracePeriodExceeded(settingsService);
final bool notifyTotalProgress = settingsService
@@ -415,6 +418,7 @@ class BackgroundService {
List toUpload = await backupService.buildUploadCandidates(
selectedAlbums,
+ excludedAlbums,
);
try {
diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart
index e7cb4d27ee..a42c587435 100644
--- a/mobile/lib/services/backup.service.dart
+++ b/mobile/lib/services/backup.service.dart
@@ -65,10 +65,14 @@ class BackupService {
QueryBuilder
selectedAlbumsQuery() =>
_db.backupAlbums.filter().selectionEqualTo(BackupSelection.select);
+ QueryBuilder
+ excludedAlbumsQuery() =>
+ _db.backupAlbums.filter().selectionEqualTo(BackupSelection.exclude);
/// Returns all assets newer than the last successful backup per album
Future> buildUploadCandidates(
List selectedBackupAlbums,
+ List excludedBackupAlbums,
) async {
final filter = FilterOptionGroup(
containsPathModified: true,
@@ -85,13 +89,19 @@ class BackupService {
}
final int allIdx = selectedAlbums.indexWhere((e) => e != null && e.isAll);
if (allIdx != -1) {
+ final List excludedAlbums =
+ await _loadAlbumsWithTimeFilter(excludedBackupAlbums, filter, now);
final List toAdd = await _fetchAssetsAndUpdateLastBackup(
selectedAlbums.slice(allIdx, allIdx + 1),
selectedBackupAlbums.slice(allIdx, allIdx + 1),
now,
);
-
- return toAdd.toSet().toList();
+ final List toRemove = await _fetchAssetsAndUpdateLastBackup(
+ excludedAlbums,
+ excludedBackupAlbums,
+ now,
+ );
+ return toAdd.toSet().difference(toRemove.toSet()).toList();
} else {
return await _fetchAssetsAndUpdateLastBackup(
selectedAlbums,
diff --git a/mobile/lib/widgets/backup/album_info_card.dart b/mobile/lib/widgets/backup/album_info_card.dart
index c65f14e953..e9349bd69e 100644
--- a/mobile/lib/widgets/backup/album_info_card.dart
+++ b/mobile/lib/widgets/backup/album_info_card.dart
@@ -1,12 +1,14 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
+import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/backup/available_album.model.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
+import 'package:immich_mobile/widgets/common/immich_toast.dart';
class AlbumInfoCard extends HookConsumerWidget {
final AvailableAlbum album;
@@ -17,6 +19,8 @@ class AlbumInfoCard extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final bool isSelected =
ref.watch(backupProvider).selectedBackupAlbums.contains(album);
+ final bool isExcluded =
+ ref.watch(backupProvider).excludedBackupAlbums.contains(album);
final isDarkTheme = context.isDarkTheme;
@@ -24,7 +28,8 @@ class AlbumInfoCard extends HookConsumerWidget {
context.primaryColor.withAlpha(100),
BlendMode.darken,
);
-
+ ColorFilter excludedFilter =
+ ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
ColorFilter unselectedFilter =
const ColorFilter.mode(Colors.black, BlendMode.color);
@@ -43,6 +48,20 @@ class AlbumInfoCard extends HookConsumerWidget {
).tr(),
backgroundColor: context.primaryColor,
);
+ } else if (isExcluded) {
+ return Chip(
+ visualDensity: VisualDensity.compact,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
+ label: Text(
+ "album_info_card_backup_album_excluded",
+ style: TextStyle(
+ fontSize: 10,
+ color: isDarkTheme ? Colors.black : Colors.white,
+ fontWeight: FontWeight.bold,
+ ),
+ ).tr(),
+ backgroundColor: Colors.red[300],
+ );
}
return const SizedBox();
@@ -51,6 +70,8 @@ class AlbumInfoCard extends HookConsumerWidget {
buildImageFilter() {
if (isSelected) {
return selectedFilter;
+ } else if (isExcluded) {
+ return excludedFilter;
} else {
return unselectedFilter;
}
@@ -66,6 +87,28 @@ class AlbumInfoCard extends HookConsumerWidget {
ref.read(backupProvider.notifier).addAlbumForBackup(album);
}
},
+ onDoubleTap: () {
+ ref.read(hapticFeedbackProvider.notifier).selectionClick();
+
+ if (isExcluded) {
+ // Remove from exclude album list
+ ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album);
+ } else {
+ // Add to exclude album list
+
+ if (album.id == 'isAll' || album.name == 'Recents') {
+ ImmichToast.show(
+ context: context,
+ msg: 'Cannot exclude album contains all assets',
+ toastType: ToastType.error,
+ gravity: ToastGravity.BOTTOM,
+ );
+ return;
+ }
+
+ ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album);
+ }
+ },
child: Card(
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.all(1),
diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart
index 69415ee126..2e10fe0b75 100644
--- a/mobile/lib/widgets/backup/album_info_list_tile.dart
+++ b/mobile/lib/widgets/backup/album_info_list_tile.dart
@@ -1,12 +1,14 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/backup/available_album.model.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
+import 'package:immich_mobile/widgets/common/immich_toast.dart';
class AlbumInfoListTile extends HookConsumerWidget {
final AvailableAlbum album;
@@ -17,6 +19,8 @@ class AlbumInfoListTile extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final bool isSelected =
ref.watch(backupProvider).selectedBackupAlbums.contains(album);
+ final bool isExcluded =
+ ref.watch(backupProvider).excludedBackupAlbums.contains(album);
var assetCount = useState(0);
useEffect(
@@ -32,6 +36,10 @@ class AlbumInfoListTile extends HookConsumerWidget {
return context.isDarkTheme
? context.primaryColor.withAlpha(100)
: context.primaryColor.withAlpha(25);
+ } else if (isExcluded) {
+ return context.isDarkTheme
+ ? Colors.red[300]?.withAlpha(150)
+ : Colors.red[100]?.withAlpha(150);
} else {
return Colors.transparent;
}
@@ -45,6 +53,13 @@ class AlbumInfoListTile extends HookConsumerWidget {
);
}
+ if (isExcluded) {
+ return const Icon(
+ Icons.remove_circle_rounded,
+ color: Colors.red,
+ );
+ }
+
return Icon(
Icons.circle,
color: context.isDarkTheme ? Colors.grey[400] : Colors.black45,
@@ -52,6 +67,28 @@ class AlbumInfoListTile extends HookConsumerWidget {
}
return GestureDetector(
+ onDoubleTap: () {
+ ref.watch(hapticFeedbackProvider.notifier).selectionClick();
+
+ if (isExcluded) {
+ // Remove from exclude album list
+ ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album);
+ } else {
+ // Add to exclude album list
+
+ if (album.id == 'isAll' || album.name == 'Recents') {
+ ImmichToast.show(
+ context: context,
+ msg: 'Cannot exclude album contains all assets',
+ toastType: ToastType.error,
+ gravity: ToastGravity.BOTTOM,
+ );
+ return;
+ }
+
+ ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album);
+ }
+ },
child: ListTile(
tileColor: buildTileColor(),
contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),