1
0
mirror of https://github.com/immich-app/immich.git synced 2025-07-16 07:24:40 +02:00

Fix backup not resuming after closed and reopen (#266)

* Fixed app not resuming backup after closing and reopening the app

* Fixed cosmetic effect of backup button doesn't change state right away after pressing start backup

* Fixed grammar

* Fixed deep copy problem that cause incorrect asset count when backing up

* Format code
This commit is contained in:
Alex
2022-06-25 15:12:47 -05:00
committed by GitHub
parent d02b97e1c1
commit 40a8115101
63 changed files with 677 additions and 300 deletions

View File

@ -21,13 +21,16 @@ class AvailableAlbum {
}
@override
String toString() => 'AvailableAlbum(albumEntity: $albumEntity, thumbnailData: $thumbnailData)';
String toString() =>
'AvailableAlbum(albumEntity: $albumEntity, thumbnailData: $thumbnailData)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is AvailableAlbum && other.albumEntity == albumEntity && other.thumbnailData == thumbnailData;
return other is AvailableAlbum &&
other.albumEntity == albumEntity &&
other.thumbnailData == thumbnailData;
}
@override

View File

@ -10,7 +10,7 @@ enum BackUpProgressEnum { idle, inProgress, done }
class BackUpState extends Equatable {
// enum
final BackUpProgressEnum backupProgress;
final List<String> allAssetOnDatabase;
final List<String> allAssetsInDatabase;
final double progressInPercentage;
final CancellationToken cancelToken;
final ServerInfo serverInfo;
@ -28,7 +28,7 @@ class BackUpState extends Equatable {
const BackUpState({
required this.backupProgress,
required this.allAssetOnDatabase,
required this.allAssetsInDatabase,
required this.progressInPercentage,
required this.cancelToken,
required this.serverInfo,
@ -41,7 +41,7 @@ class BackUpState extends Equatable {
BackUpState copyWith({
BackUpProgressEnum? backupProgress,
List<String>? allAssetOnDatabase,
List<String>? allAssetsInDatabase,
double? progressInPercentage,
CancellationToken? cancelToken,
ServerInfo? serverInfo,
@ -53,7 +53,7 @@ class BackUpState extends Equatable {
}) {
return BackUpState(
backupProgress: backupProgress ?? this.backupProgress,
allAssetOnDatabase: allAssetOnDatabase ?? this.allAssetOnDatabase,
allAssetsInDatabase: allAssetsInDatabase ?? this.allAssetsInDatabase,
progressInPercentage: progressInPercentage ?? this.progressInPercentage,
cancelToken: cancelToken ?? this.cancelToken,
serverInfo: serverInfo ?? this.serverInfo,
@ -61,20 +61,21 @@ class BackUpState extends Equatable {
selectedBackupAlbums: selectedBackupAlbums ?? this.selectedBackupAlbums,
excludedBackupAlbums: excludedBackupAlbums ?? this.excludedBackupAlbums,
allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets,
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds,
selectedAlbumsBackupAssetsIds:
selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds,
);
}
@override
String toString() {
return 'BackUpState(backupProgress: $backupProgress, allAssetOnDatabase: $allAssetOnDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds)';
return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds)';
}
@override
List<Object> get props {
return [
backupProgress,
allAssetOnDatabase,
allAssetsInDatabase,
progressInPercentage,
cancelToken,
serverInfo,

View File

@ -19,7 +19,8 @@ class HiveBackupAlbums {
});
@override
String toString() => 'HiveBackupAlbums(selectedAlbumIds: $selectedAlbumIds, excludedAlbumsIds: $excludedAlbumsIds)';
String toString() =>
'HiveBackupAlbums(selectedAlbumIds: $selectedAlbumIds, excludedAlbumsIds: $excludedAlbumsIds)';
HiveBackupAlbums copyWith({
List<String>? selectedAlbumIds,
@ -49,7 +50,8 @@ class HiveBackupAlbums {
String toJson() => json.encode(toMap());
factory HiveBackupAlbums.fromJson(String source) => HiveBackupAlbums.fromMap(json.decode(source));
factory HiveBackupAlbums.fromJson(String source) =>
HiveBackupAlbums.fromMap(json.decode(source));
@override
bool operator ==(Object other) {

View File

@ -18,7 +18,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
: super(
BackUpState(
backupProgress: BackUpProgressEnum.idle,
allAssetOnDatabase: const [],
allAssetsInDatabase: const [],
progressInPercentage: 0,
cancelToken: CancellationToken(),
serverInfo: ServerInfo(
@ -36,7 +36,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
allUniqueAssets: const {},
selectedAlbumsBackupAssetsIds: const {},
),
);
) {
getBackupInfo();
}
final BackupService _backupService;
final ServerInfoService _serverInfoService;
@ -93,7 +95,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// If this is the first time performing backup - set the default selected album to be
/// the one that has all assets (Recent on Android, Recents on iOS)
///
Future<void> getBackupAlbumsInfo() async {
Future<void> _getBackupAlbumsInfo() async {
// Get all albums on the device
List<AvailableAlbum> availableAlbums = [];
List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
@ -177,7 +179,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// Find the assets that are not overlapping between the two sets
/// Those assets are unique and are used as the total assets
///
void _updateBackupAssetCount() async {
Future<void> _updateBackupAssetCount() async {
Set<AssetEntity> assetsFromSelectedAlbums = {};
Set<AssetEntity> assetsFromExcludedAlbums = {};
@ -195,27 +197,27 @@ class BackupNotifier extends StateNotifier<BackUpState> {
Set<AssetEntity> allUniqueAssets =
assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
List<String> allAssetOnDatabase =
List<String> allAssetsInDatabase =
await _backupService.getDeviceBackupAsset();
// Find asset that were backup from selected albums
Set<String> selectedAlbumsBackupAssets =
Set.from(allUniqueAssets.map((e) => e.id));
selectedAlbumsBackupAssets
.removeWhere((assetId) => !allAssetOnDatabase.contains(assetId));
.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
if (allUniqueAssets.isEmpty) {
debugPrint("No Asset On Device");
state = state.copyWith(
backupProgress: BackUpProgressEnum.idle,
allAssetOnDatabase: allAssetOnDatabase,
allAssetsInDatabase: allAssetsInDatabase,
allUniqueAssets: {},
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
);
return;
} else {
state = state.copyWith(
allAssetOnDatabase: allAssetOnDatabase,
allAssetsInDatabase: allAssetsInDatabase,
allUniqueAssets: allUniqueAssets,
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
);
@ -223,6 +225,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
// Save to persistent storage
_updatePersistentAlbumsSelection();
return;
}
///
@ -230,10 +234,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// which albums are selected or excluded
/// and then update the UI according to those information
///
void getBackupInfo() async {
await getBackupAlbumsInfo();
_updateServerInfo();
_updateBackupAssetCount();
Future<void> getBackupInfo() async {
await _getBackupAlbumsInfo();
await _updateServerInfo();
await _updateBackupAssetCount();
}
///
@ -256,11 +260,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// Invoke backup process
///
void startBackupProcess() async {
_updateServerInfo();
_updateBackupAssetCount();
state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
await getBackupInfo();
var authResult = await PhotoManager.requestPermissionExtend();
if (authResult.isAuth) {
await PhotoManager.clearFileCache();
@ -271,10 +274,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
return;
}
Set<AssetEntity> assetsWillBeBackup = state.allUniqueAssets;
Set<AssetEntity> assetsWillBeBackup = Set.from(state.allUniqueAssets);
// Remove item that has already been backed up
for (var assetId in state.allAssetOnDatabase) {
for (var assetId in state.allAssetsInDatabase) {
assetsWillBeBackup.removeWhere((e) => e.id == assetId);
}
@ -301,8 +304,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
state = state.copyWith(selectedAlbumsBackupAssetsIds: {
...state.selectedAlbumsBackupAssetsIds,
deviceAssetId
}, allAssetOnDatabase: [
...state.allAssetOnDatabase,
}, allAssetsInDatabase: [
...state.allAssetsInDatabase,
deviceAssetId
]);
@ -321,7 +324,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
progressInPercentage: (sent.toDouble() / total.toDouble() * 100));
}
void _updateServerInfo() async {
Future<void> _updateServerInfo() async {
var serverInfo = await _serverInfoService.getServerInfo();
// Update server info

View File

@ -14,16 +14,22 @@ class AlbumInfoCard extends HookConsumerWidget {
final Uint8List? imageData;
final AssetPathEntity albumInfo;
const AlbumInfoCard({Key? key, this.imageData, required this.albumInfo}) : super(key: key);
const AlbumInfoCard({Key? key, this.imageData, required this.albumInfo})
: super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final bool isSelected = ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
final bool isSelected =
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
final bool isExcluded =
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
ColorFilter selectedFilter = ColorFilter.mode(Theme.of(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);
ColorFilter selectedFilter = ColorFilter.mode(
Theme.of(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);
_buildSelectedTextBox() {
if (isSelected) {
@ -32,7 +38,8 @@ class AlbumInfoCard extends HookConsumerWidget {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
label: const Text(
"INCLUDED",
style: TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
style: TextStyle(
fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
),
backgroundColor: Theme.of(context).primaryColor,
);
@ -42,7 +49,8 @@ class AlbumInfoCard extends HookConsumerWidget {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
label: const Text(
"EXCLUDED",
style: TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
style: TextStyle(
fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
),
backgroundColor: Colors.red[300],
);
@ -85,10 +93,15 @@ class AlbumInfoCard extends HookConsumerWidget {
HapticFeedback.selectionClick();
if (isExcluded) {
ref.watch(backupProvider.notifier).removeExcludedAlbumForBackup(albumInfo);
ref
.watch(backupProvider.notifier)
.removeExcludedAlbumForBackup(albumInfo);
} else {
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1 &&
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo)) {
ref
.watch(backupProvider)
.selectedBackupAlbums
.contains(albumInfo)) {
ImmichToast.show(
context: context,
msg: "Cannot exclude the only album",
@ -98,7 +111,9 @@ class AlbumInfoCard extends HookConsumerWidget {
return;
}
ref.watch(backupProvider.notifier).addExcludedAlbumForBackup(albumInfo);
ref
.watch(backupProvider.notifier)
.addExcludedAlbumForBackup(albumInfo);
}
},
child: Card(
@ -121,12 +136,16 @@ class AlbumInfoCard extends HookConsumerWidget {
width: 200,
height: 200,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12)),
image: DecorationImage(
colorFilter: _buildImageFilter(),
image: imageData != null
? MemoryImage(imageData!)
: const AssetImage('assets/immich-logo-no-outline.png') as ImageProvider,
: const AssetImage(
'assets/immich-logo-no-outline.png')
as ImageProvider,
fit: BoxFit.cover,
),
),
@ -150,13 +169,17 @@ class AlbumInfoCard extends HookConsumerWidget {
Text(
albumInfo.name,
style: TextStyle(
fontSize: 14, color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold),
fontSize: 14,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold),
),
Padding(
padding: const EdgeInsets.only(top: 2.0),
child: Text(
albumInfo.assetCount.toString() + (albumInfo.isAll ? " (ALL)" : ""),
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
albumInfo.assetCount.toString() +
(albumInfo.isAll ? " (ALL)" : ""),
style: TextStyle(
fontSize: 12, color: Colors.grey[600]),
),
)
],
@ -165,7 +188,8 @@ class AlbumInfoCard extends HookConsumerWidget {
),
IconButton(
onPressed: () {
AutoRouter.of(context).push(AlbumPreviewRoute(album: albumInfo));
AutoRouter.of(context)
.push(AlbumPreviewRoute(album: albumInfo));
},
icon: Icon(
Icons.image_outlined,

View File

@ -4,7 +4,12 @@ class BackupInfoCard extends StatelessWidget {
final String title;
final String subtitle;
final String info;
const BackupInfoCard({Key? key, required this.title, required this.subtitle, required this.info}) : super(key: key);
const BackupInfoCard(
{Key? key,
required this.title,
required this.subtitle,
required this.info})
: super(key: key);
@override
Widget build(BuildContext context) {

View File

@ -16,7 +16,8 @@ class AlbumPreviewPage extends HookConsumerWidget {
final assets = useState<List<AssetEntity>>([]);
_getAssetsInAlbum() async {
assets.value = await album.getAssetListRange(start: 0, end: album.assetCount);
assets.value =
await album.getAssetListRange(start: 0, end: album.assetCount);
}
useEffect(() {
@ -37,7 +38,10 @@ class AlbumPreviewPage extends HookConsumerWidget {
padding: const EdgeInsets.only(top: 4.0),
child: Text(
"ID ${album.id}",
style: TextStyle(fontSize: 10, color: Colors.grey[600], fontWeight: FontWeight.bold),
style: TextStyle(
fontSize: 10,
color: Colors.grey[600],
fontWeight: FontWeight.bold),
),
),
],
@ -55,8 +59,9 @@ class AlbumPreviewPage extends HookConsumerWidget {
),
itemCount: assets.value.length,
itemBuilder: (context, index) {
Future<Uint8List?> thumbData =
assets.value[index].thumbnailDataWithSize(const ThumbnailSize(200, 200), quality: 50);
Future<Uint8List?> thumbData = assets.value[index]
.thumbnailDataWithSize(const ThumbnailSize(200, 200),
quality: 50);
return FutureBuilder<Uint8List?>(
future: thumbData,

View File

@ -17,7 +17,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
useEffect(() {
ref.read(backupProvider.notifier).getBackupAlbumsInfo();
ref.read(backupProvider.notifier).getBackupInfo();
return null;
}, []);
@ -37,8 +37,12 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
itemBuilder: ((context, index) {
var thumbnailData = availableAlbums[index].thumbnailData;
return Padding(
padding: index == 0 ? const EdgeInsets.only(left: 16.00) : const EdgeInsets.all(0),
child: AlbumInfoCard(imageData: thumbnailData, albumInfo: availableAlbums[index].albumEntity),
padding: index == 0
? const EdgeInsets.only(left: 16.00)
: const EdgeInsets.all(0),
child: AlbumInfoCard(
imageData: thumbnailData,
albumInfo: availableAlbums[index].albumEntity),
);
}),
),
@ -67,10 +71,14 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
onTap: removeSelection,
child: Chip(
visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)),
label: Text(
album.name,
style: const TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 10,
color: Colors.white,
fontWeight: FontWeight.bold),
),
backgroundColor: Theme.of(context).primaryColor,
deleteIconColor: Colors.white,
@ -88,7 +96,9 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
_buildExcludedAlbumNameChip() {
return excludedBackupAlbums.map((album) {
void removeSelection() {
ref.watch(backupProvider.notifier).removeExcludedAlbumForBackup(album);
ref
.watch(backupProvider.notifier)
.removeExcludedAlbumForBackup(album);
}
return GestureDetector(
@ -97,10 +107,14 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
padding: const EdgeInsets.only(right: 8.0),
child: Chip(
visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)),
label: Text(
album.name,
style: const TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 10,
color: Colors.white,
fontWeight: FontWeight.bold),
),
backgroundColor: Colors.red[300],
deleteIconColor: Colors.white,
@ -142,7 +156,10 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Wrap(
children: [..._buildSelectedAlbumNameChip(), ..._buildExcludedAlbumNameChip()],
children: [
..._buildSelectedAlbumNameChip(),
..._buildExcludedAlbumNameChip()
],
),
),
@ -165,10 +182,17 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
visualDensity: VisualDensity.compact,
title: Text(
"Total unique assets",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: Colors.grey[700]),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Colors.grey[700]),
),
trailing: Text(
ref.watch(backupProvider).allUniqueAssets.length.toString(),
ref
.watch(backupProvider)
.allUniqueAssets
.length
.toString(),
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
@ -206,7 +230,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
context: context,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
elevation: 5,
title: Text(
'Selection Info',
@ -221,7 +246,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
children: [
Text(
'Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.',
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
style: TextStyle(
fontSize: 14, color: Colors.grey[700]),
),
],
),

View File

@ -26,7 +26,7 @@ class BackupControllerPage extends HookConsumerWidget {
useEffect(() {
if (backupState.backupProgress != BackUpProgressEnum.inProgress) {
ref.read(backupProvider.notifier).getBackupInfo();
ref.watch(backupProvider.notifier).getBackupInfo();
}
ref
@ -112,13 +112,15 @@ class BackupControllerPage extends HookConsumerWidget {
),
),
onPressed: () {
isAutoBackup
? ref
.watch(authenticationProvider.notifier)
.setAutoBackup(false)
: ref
.watch(authenticationProvider.notifier)
.setAutoBackup(true);
if (isAutoBackup) {
ref
.read(authenticationProvider.notifier)
.setAutoBackup(false);
} else {
ref
.read(authenticationProvider.notifier)
.setAutoBackup(true);
}
},
child: Text("Turn $backupBtnText Backup",
style: const TextStyle(fontWeight: FontWeight.bold)),
@ -212,7 +214,7 @@ class BackupControllerPage extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Albums to be backup",
"Albums to be backed up",
style: TextStyle(color: Color(0xFF808080), fontSize: 12),
),
_buildSelectedAlbumName(),
@ -282,14 +284,12 @@ class BackupControllerPage extends HookConsumerWidget {
),
BackupInfoCard(
title: "Backup",
subtitle:
"Photos and videos from selected albums that are backup",
subtitle: "Backed up photos and videos",
info: "${backupState.selectedAlbumsBackupAssetsIds.length}",
),
BackupInfoCard(
title: "Remainder",
subtitle:
"Photos and videos that has not been backing up from selected albums",
subtitle: "Remaining photos and albums to back up from selection",
info:
"${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
),