1
0
mirror of https://github.com/immich-app/immich.git synced 2024-11-28 09:33:27 +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 { class AvailableAlbum {
final AssetPathEntity albumEntity; final AssetPathEntity albumEntity;
final DateTime? lastBackup; final DateTime? lastBackup;
final Uint8List? thumbnailData;
AvailableAlbum({ AvailableAlbum({
required this.albumEntity, required this.albumEntity,
this.lastBackup, this.lastBackup,
this.thumbnailData,
}); });
AvailableAlbum copyWith({ AvailableAlbum copyWith({
@ -20,7 +18,6 @@ class AvailableAlbum {
return AvailableAlbum( return AvailableAlbum(
albumEntity: albumEntity ?? this.albumEntity, albumEntity: albumEntity ?? this.albumEntity,
lastBackup: lastBackup ?? this.lastBackup, lastBackup: lastBackup ?? this.lastBackup,
thumbnailData: thumbnailData ?? this.thumbnailData,
); );
} }
@ -34,7 +31,7 @@ class AvailableAlbum {
@override @override
String toString() => String toString() =>
'AvailableAlbum(albumEntity: $albumEntity, lastBackup: $lastBackup, thumbnailData: $thumbnailData)'; 'AvailableAlbum(albumEntity: $albumEntity, lastBackup: $lastBackup)';
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {

View File

@ -234,34 +234,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
for (AssetPathEntity album in albums) { for (AssetPathEntity album in albums) {
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album); AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
final assetCountInAlbum = await album.assetCountAsync;
if (assetCountInAlbum > 0) {
final assetList = await album.getAssetListPaged(page: 0, size: 1);
// 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); availableAlbums.add(availableAlbum);
albumMap[album.id] = album; albumMap[album.id] = album;
} }
}
state = state.copyWith(availableAlbums: availableAlbums); state = state.copyWith(availableAlbums: availableAlbums);
final List<BackupAlbum> excludedBackupAlbums = final List<BackupAlbum> excludedBackupAlbums =

View File

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

View File

@ -11,47 +11,26 @@ import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart';
class AlbumInfoListTile extends HookConsumerWidget { class AlbumInfoListTile extends HookConsumerWidget {
final Uint8List? imageData; final AvailableAlbum album;
final AvailableAlbum albumInfo;
const AlbumInfoListTile({super.key, this.imageData, required this.albumInfo}); const AlbumInfoListTile({super.key, required this.album});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final bool isSelected = final bool isSelected =
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo); ref.watch(backupProvider).selectedBackupAlbums.contains(album);
final bool isExcluded = final bool isExcluded =
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo); ref.watch(backupProvider).excludedBackupAlbums.contains(album);
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);
var assetCount = useState(0); var assetCount = useState(0);
useEffect( useEffect(
() { () {
albumInfo.assetCount.then((value) => assetCount.value = value); album.assetCount.then((value) => assetCount.value = value);
return null; return null;
}, },
[albumInfo], [album],
); );
buildImageFilter() {
if (isSelected) {
return selectedFilter;
} else if (isExcluded) {
return excludedFilter;
} else {
return unselectedFilter;
}
}
buildTileColor() { buildTileColor() {
if (isSelected) { if (isSelected) {
return context.isDarkTheme 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( return GestureDetector(
onDoubleTap: () { onDoubleTap: () {
HapticFeedback.selectionClick(); HapticFeedback.selectionClick();
if (isExcluded) { if (isExcluded) {
// Remove from exclude album list // Remove from exclude album list
ref ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album);
.read(backupProvider.notifier)
.removeExcludedAlbumForBackup(albumInfo);
} else { } else {
// Add to exclude album list // Add to exclude album list
if (albumInfo.id == 'isAll' || albumInfo.name == 'Recents') { if (album.id == 'isAll' || album.name == 'Recents') {
ImmichToast.show( ImmichToast.show(
context: context, context: context,
msg: 'Cannot exclude album contains all assets', msg: 'Cannot exclude album contains all assets',
@ -88,9 +86,7 @@ class AlbumInfoListTile extends HookConsumerWidget {
return; return;
} }
ref ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album);
.read(backupProvider.notifier)
.addExcludedAlbumForBackup(albumInfo);
} }
}, },
child: ListTile( child: ListTile(
@ -99,33 +95,14 @@ class AlbumInfoListTile extends HookConsumerWidget {
onTap: () { onTap: () {
HapticFeedback.selectionClick(); HapticFeedback.selectionClick();
if (isSelected) { if (isSelected) {
ref.read(backupProvider.notifier).removeAlbumForBackup(albumInfo); ref.read(backupProvider.notifier).removeAlbumForBackup(album);
} else { } else {
ref.read(backupProvider.notifier).addAlbumForBackup(albumInfo); ref.read(backupProvider.notifier).addAlbumForBackup(album);
} }
}, },
leading: ClipRRect( leading: buildIcon(),
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,
),
),
),
),
title: Text( title: Text(
albumInfo.name, album.name,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -135,7 +112,7 @@ class AlbumInfoListTile extends HookConsumerWidget {
trailing: IconButton( trailing: IconButton(
onPressed: () { onPressed: () {
context.pushRoute( context.pushRoute(
AlbumPreviewRoute(album: albumInfo.albumEntity), AlbumPreviewRoute(album: album.albumEntity),
); );
}, },
icon: Icon( icon: Icon(

View File

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

View File

@ -26,7 +26,7 @@ class BackupControllerPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
BackUpState backupState = ref.watch(backupProvider); BackUpState backupState = ref.watch(backupProvider);
final hasAnyAlbum = backupState.selectedBackupAlbums.isNotEmpty; final hasAnyAlbum = backupState.selectedBackupAlbums.isNotEmpty;
final didGetBackupInfo = useState(false);
bool hasExclusiveAccess = bool hasExclusiveAccess =
backupState.backupProgress != BackUpProgressEnum.inBackground; backupState.backupProgress != BackUpProgressEnum.inBackground;
bool shouldBackup = backupState.allUniqueAssets.length - bool shouldBackup = backupState.allUniqueAssets.length -
@ -38,11 +38,6 @@ class BackupControllerPage extends HookConsumerWidget {
useEffect( 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 // Update the background settings information just to make sure we
// have the latest, since the platform channel will not update // have the latest, since the platform channel will not update
// automatically // 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() { Widget buildSelectedAlbumName() {
var text = "backup_controller_page_backup_selected".tr(); var text = "backup_controller_page_backup_selected".tr();
var albums = ref.watch(backupProvider).selectedBackupAlbums; 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( return Scaffold(
appBar: AppBar( appBar: AppBar(
elevation: 0, elevation: 0,
@ -297,7 +313,10 @@ class BackupControllerPage extends HookConsumerWidget {
if (!hasExclusiveAccess) buildBackgroundBackupInfo(), if (!hasExclusiveAccess) buildBackgroundBackupInfo(),
buildBackupButton(), buildBackupButton(),
] ]
: [buildFolderSelectionTile()], : [
buildFolderSelectionTile(),
if (!didGetBackupInfo.value) buildLoadingIndicator(),
],
), ),
), ),
); );