1
0
mirror of https://github.com/immich-app/immich.git synced 2024-11-24 08:52:28 +02:00

refactor(mobile): backup album selection (#8053)

* feat(mobile): include album with 0 assets as album option for backup

* Show icon instead of thumbnail

* Handle backupProgress state transition correctly to always load the backup info

* remove todo comment
This commit is contained in:
Alex 2024-03-19 08:40:14 -05:00 committed by GitHub
parent c6d2408517
commit 0bc773fd00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 437 additions and 126 deletions

File diff suppressed because one or more lines are too long

View File

@ -5,11 +5,9 @@ import 'package:photo_manager/photo_manager.dart';
class AvailableAlbum {
final AssetPathEntity albumEntity;
final DateTime? lastBackup;
final Uint8List? thumbnailData;
AvailableAlbum({
required this.albumEntity,
this.lastBackup,
this.thumbnailData,
});
AvailableAlbum copyWith({
@ -20,7 +18,6 @@ class AvailableAlbum {
return AvailableAlbum(
albumEntity: albumEntity ?? this.albumEntity,
lastBackup: lastBackup ?? this.lastBackup,
thumbnailData: thumbnailData ?? this.thumbnailData,
);
}
@ -34,7 +31,7 @@ class AvailableAlbum {
@override
String toString() =>
'AvailableAlbum(albumEntity: $albumEntity, lastBackup: $lastBackup, thumbnailData: $thumbnailData)';
'AvailableAlbum(albumEntity: $albumEntity, lastBackup: $lastBackup)';
@override
bool operator ==(Object other) {

View File

@ -234,33 +234,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
for (AssetPathEntity album in albums) {
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
final assetCountInAlbum = await album.assetCountAsync;
if (assetCountInAlbum > 0) {
final assetList = await album.getAssetListPaged(page: 0, size: 1);
availableAlbums.add(availableAlbum);
// Even though we check assetCountInAlbum to make sure that there are assets in album
// The `getAssetListPaged` method still return empty list and cause not assets get rendered
if (assetList.isEmpty) {
continue;
}
final thumbnailAsset = assetList.first;
try {
final thumbnailData = await thumbnailAsset
.thumbnailDataWithSize(const ThumbnailSize(512, 512));
availableAlbum =
availableAlbum.copyWith(thumbnailData: thumbnailData);
} catch (e, stack) {
log.severe(
"Failed to get thumbnail for album ${album.name}",
e,
stack,
);
}
availableAlbums.add(availableAlbum);
albumMap[album.id] = album;
}
albumMap[album.id] = album;
}
state = state.copyWith(availableAlbums: availableAlbums);

View File

@ -11,17 +11,16 @@ import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
class AlbumInfoCard extends HookConsumerWidget {
final Uint8List? imageData;
final AvailableAlbum albumInfo;
final AvailableAlbum album;
const AlbumInfoCard({super.key, this.imageData, required this.albumInfo});
const AlbumInfoCard({super.key, required this.album});
@override
Widget build(BuildContext context, WidgetRef ref) {
final bool isSelected =
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
ref.watch(backupProvider).selectedBackupAlbums.contains(album);
final bool isExcluded =
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
ref.watch(backupProvider).excludedBackupAlbums.contains(album);
final isDarkTheme = context.isDarkTheme;
ColorFilter selectedFilter = ColorFilter.mode(
@ -82,9 +81,9 @@ class AlbumInfoCard extends HookConsumerWidget {
HapticFeedback.selectionClick();
if (isSelected) {
ref.read(backupProvider.notifier).removeAlbumForBackup(albumInfo);
ref.read(backupProvider.notifier).removeAlbumForBackup(album);
} else {
ref.read(backupProvider.notifier).addAlbumForBackup(albumInfo);
ref.read(backupProvider.notifier).addAlbumForBackup(album);
}
},
onDoubleTap: () {
@ -92,13 +91,11 @@ class AlbumInfoCard extends HookConsumerWidget {
if (isExcluded) {
// Remove from exclude album list
ref
.read(backupProvider.notifier)
.removeExcludedAlbumForBackup(albumInfo);
ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album);
} else {
// Add to exclude album list
if (albumInfo.id == 'isAll' || albumInfo.name == 'Recents') {
if (album.id == 'isAll' || album.name == 'Recents') {
ImmichToast.show(
context: context,
msg: 'Cannot exclude album contains all assets',
@ -108,9 +105,7 @@ class AlbumInfoCard extends HookConsumerWidget {
return;
}
ref
.read(backupProvider.notifier)
.addExcludedAlbumForBackup(albumInfo);
ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album);
}
},
child: Card(
@ -136,14 +131,12 @@ class AlbumInfoCard extends HookConsumerWidget {
children: [
ColorFiltered(
colorFilter: buildImageFilter(),
child: Image(
child: const Image(
width: double.infinity,
height: double.infinity,
image: imageData != null
? MemoryImage(imageData!)
: const AssetImage(
'assets/immich-logo.png',
) as ImageProvider,
image: AssetImage(
'assets/immich-logo.png',
),
fit: BoxFit.cover,
),
),
@ -168,7 +161,7 @@ class AlbumInfoCard extends HookConsumerWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
albumInfo.name,
album.name,
style: TextStyle(
fontSize: 14,
color: context.primaryColor,
@ -182,7 +175,7 @@ class AlbumInfoCard extends HookConsumerWidget {
if (snapshot.hasData) {
return Text(
snapshot.data.toString() +
(albumInfo.isAll
(album.isAll
? " (${'backup_all'.tr()})"
: ""),
style: TextStyle(
@ -193,7 +186,7 @@ class AlbumInfoCard extends HookConsumerWidget {
}
return const Text("0");
}),
future: albumInfo.assetCount,
future: album.assetCount,
),
),
],
@ -202,7 +195,7 @@ class AlbumInfoCard extends HookConsumerWidget {
IconButton(
onPressed: () {
context.pushRoute(
AlbumPreviewRoute(album: albumInfo.albumEntity),
AlbumPreviewRoute(album: album.albumEntity),
);
},
icon: Icon(

View File

@ -11,47 +11,26 @@ import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
class AlbumInfoListTile extends HookConsumerWidget {
final Uint8List? imageData;
final AvailableAlbum albumInfo;
final AvailableAlbum album;
const AlbumInfoListTile({super.key, this.imageData, required this.albumInfo});
const AlbumInfoListTile({super.key, required this.album});
@override
Widget build(BuildContext context, WidgetRef ref) {
final bool isSelected =
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
ref.watch(backupProvider).selectedBackupAlbums.contains(album);
final bool isExcluded =
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
ColorFilter selectedFilter = ColorFilter.mode(
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);
ref.watch(backupProvider).excludedBackupAlbums.contains(album);
var assetCount = useState(0);
useEffect(
() {
albumInfo.assetCount.then((value) => assetCount.value = value);
album.assetCount.then((value) => assetCount.value = value);
return null;
},
[albumInfo],
[album],
);
buildImageFilter() {
if (isSelected) {
return selectedFilter;
} else if (isExcluded) {
return excludedFilter;
} else {
return unselectedFilter;
}
}
buildTileColor() {
if (isSelected) {
return context.isDarkTheme
@ -66,19 +45,38 @@ class AlbumInfoListTile extends HookConsumerWidget {
}
}
buildIcon() {
if (isSelected) {
return const Icon(
Icons.check_circle_rounded,
color: Colors.green,
);
}
if (isExcluded) {
return const Icon(
Icons.remove_circle_rounded,
color: Colors.red,
);
}
return Icon(
Icons.circle,
color: context.isDarkTheme ? Colors.grey[400] : Colors.black45,
);
}
return GestureDetector(
onDoubleTap: () {
HapticFeedback.selectionClick();
if (isExcluded) {
// Remove from exclude album list
ref
.read(backupProvider.notifier)
.removeExcludedAlbumForBackup(albumInfo);
ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album);
} else {
// Add to exclude album list
if (albumInfo.id == 'isAll' || albumInfo.name == 'Recents') {
if (album.id == 'isAll' || album.name == 'Recents') {
ImmichToast.show(
context: context,
msg: 'Cannot exclude album contains all assets',
@ -88,9 +86,7 @@ class AlbumInfoListTile extends HookConsumerWidget {
return;
}
ref
.read(backupProvider.notifier)
.addExcludedAlbumForBackup(albumInfo);
ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album);
}
},
child: ListTile(
@ -99,33 +95,14 @@ class AlbumInfoListTile extends HookConsumerWidget {
onTap: () {
HapticFeedback.selectionClick();
if (isSelected) {
ref.read(backupProvider.notifier).removeAlbumForBackup(albumInfo);
ref.read(backupProvider.notifier).removeAlbumForBackup(album);
} else {
ref.read(backupProvider.notifier).addAlbumForBackup(albumInfo);
ref.read(backupProvider.notifier).addAlbumForBackup(album);
}
},
leading: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SizedBox(
height: 80,
width: 80,
child: ColorFiltered(
colorFilter: buildImageFilter(),
child: Image(
width: double.infinity,
height: double.infinity,
image: imageData != null
? MemoryImage(imageData!)
: const AssetImage(
'assets/immich-logo.png',
) as ImageProvider,
fit: BoxFit.cover,
),
),
),
),
leading: buildIcon(),
title: Text(
albumInfo.name,
album.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
@ -135,7 +112,7 @@ class AlbumInfoListTile extends HookConsumerWidget {
trailing: IconButton(
onPressed: () {
context.pushRoute(
AlbumPreviewRoute(album: albumInfo.albumEntity),
AlbumPreviewRoute(album: album.albumEntity),
);
},
icon: Icon(

View File

@ -43,10 +43,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
((context, index) {
var thumbnailData = albums[index].thumbnailData;
return AlbumInfoListTile(
imageData: thumbnailData,
albumInfo: albums[index],
album: albums[index],
);
}),
childCount: albums.length,
@ -74,10 +72,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
),
itemCount: albums.length,
itemBuilder: ((context, index) {
var thumbnailData = albums[index].thumbnailData;
return AlbumInfoCard(
imageData: thumbnailData,
albumInfo: albums[index],
album: albums[index],
);
}),
),

View File

@ -26,7 +26,7 @@ class BackupControllerPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
BackUpState backupState = ref.watch(backupProvider);
final hasAnyAlbum = backupState.selectedBackupAlbums.isNotEmpty;
final didGetBackupInfo = useState(false);
bool hasExclusiveAccess =
backupState.backupProgress != BackUpProgressEnum.inBackground;
bool shouldBackup = backupState.allUniqueAssets.length -
@ -38,11 +38,6 @@ class BackupControllerPage extends HookConsumerWidget {
useEffect(
() {
if (backupState.backupProgress != BackUpProgressEnum.inProgress &&
backupState.backupProgress != BackUpProgressEnum.manualInProgress) {
ref.watch(backupProvider.notifier).getBackupInfo();
}
// Update the background settings information just to make sure we
// have the latest, since the platform channel will not update
// automatically
@ -58,6 +53,18 @@ class BackupControllerPage extends HookConsumerWidget {
[],
);
useEffect(
() {
if (backupState.backupProgress == BackUpProgressEnum.idle &&
!didGetBackupInfo.value) {
ref.watch(backupProvider.notifier).getBackupInfo();
didGetBackupInfo.value = true;
}
return null;
},
[backupState.backupProgress],
);
Widget buildSelectedAlbumName() {
var text = "backup_controller_page_backup_selected".tr();
var albums = ref.watch(backupProvider).selectedBackupAlbums;
@ -235,6 +242,15 @@ class BackupControllerPage extends HookConsumerWidget {
);
}
buildLoadingIndicator() {
return const Padding(
padding: EdgeInsets.only(top: 42.0),
child: Center(
child: CircularProgressIndicator(),
),
);
}
return Scaffold(
appBar: AppBar(
elevation: 0,
@ -297,7 +313,10 @@ class BackupControllerPage extends HookConsumerWidget {
if (!hasExclusiveAccess) buildBackgroundBackupInfo(),
buildBackupButton(),
]
: [buildFolderSelectionTile()],
: [
buildFolderSelectionTile(),
if (!didGetBackupInfo.value) buildLoadingIndicator(),
],
),
),
);