mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(mobile): Add to album from asset detail view (#1413)
* add to album from asset detail view * layout and design * added shared albums * fixed remote, asset update, and hit test * made static size * fixed create album * suppress shared expansion tile if there are no shared albums * updates album * padding on tile
This commit is contained in:
		
							
								
								
									
										129
									
								
								mobile/lib/modules/album/ui/add_to_album_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								mobile/lib/modules/album/ui/add_to_album_list.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/album.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/services/album.service.dart'; | ||||
| import 'package:immich_mobile/modules/album/ui/album_thumbnail_listtile.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/shared/models/asset.dart'; | ||||
| import 'package:immich_mobile/shared/ui/drag_sheet.dart'; | ||||
| import 'package:immich_mobile/shared/ui/immich_toast.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| class AddToAlbumList extends HookConsumerWidget { | ||||
|  | ||||
|   /// The asset to add to an album | ||||
|   final Asset asset; | ||||
|  | ||||
|   const AddToAlbumList({ | ||||
|     Key? key, | ||||
|     required this.asset, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final albums = ref.watch(albumProvider); | ||||
|     final albumService = ref.watch(albumServiceProvider); | ||||
|     final sharedAlbums = ref.watch(sharedAlbumProvider); | ||||
|  | ||||
|     useEffect( | ||||
|       () { | ||||
|         // Fetch album updates, e.g., cover image | ||||
|         ref.read(albumProvider.notifier).getAllAlbums(); | ||||
|         ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); | ||||
|  | ||||
|         return null; | ||||
|       }, | ||||
|       [],  | ||||
|     ); | ||||
|  | ||||
|     void addToAlbum(AlbumResponseDto album) async { | ||||
|       final result = await albumService.addAdditionalAssetToAlbum( | ||||
|         [asset], | ||||
|         album.id, | ||||
|       ); | ||||
|        | ||||
|       if (result != null) { | ||||
|         if (result.alreadyInAlbum.isNotEmpty) { | ||||
|           ImmichToast.show( | ||||
|             context: context, | ||||
|             msg: 'Already in ${album.albumName}', | ||||
|           ); | ||||
|         } else { | ||||
|           ImmichToast.show( | ||||
|             context: context, | ||||
|             msg: 'Added to ${album.albumName}', | ||||
|           ); | ||||
|         } | ||||
|       }  | ||||
|  | ||||
|       ref.read(albumProvider.notifier).getAllAlbums(); | ||||
|       ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); | ||||
|  | ||||
|       Navigator.pop(context); | ||||
|     } | ||||
|  | ||||
|     return Card( | ||||
|       shape: const RoundedRectangleBorder( | ||||
|         borderRadius: BorderRadius.only( | ||||
|           topLeft: Radius.circular(15), | ||||
|           topRight: Radius.circular(15), | ||||
|         ), | ||||
|       ), | ||||
|       child: ListView( | ||||
|         padding: const EdgeInsets.all(18.0), | ||||
|         children: [ | ||||
|           Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: [ | ||||
|               const Align( | ||||
|                 alignment: Alignment.center, | ||||
|                 child: CustomDraggingHandle(), | ||||
|               ), | ||||
|               const SizedBox(height: 12), | ||||
|               Text('Add to album', | ||||
|                 style: Theme.of(context).textTheme.headline1, | ||||
|               ), | ||||
|               TextButton.icon( | ||||
|                 icon: const Icon(Icons.add), | ||||
|                 label: const Text('New album'), | ||||
|                 onPressed: () { | ||||
|                   ref.watch(assetSelectionProvider.notifier).removeAll(); | ||||
|                   ref.watch(assetSelectionProvider.notifier).addNewAssets([asset]); | ||||
|                   AutoRouter.of(context).push( | ||||
|                     CreateAlbumRoute( | ||||
|                       isSharedAlbum: false, | ||||
|                       initialAssets: [asset], | ||||
|                     ), | ||||
|                   ); | ||||
|                 }, | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|           if (sharedAlbums.isNotEmpty) | ||||
|             ExpansionTile( | ||||
|               title: const Text('Shared'), | ||||
|               tilePadding: const EdgeInsets.symmetric(horizontal: 10.0), | ||||
|               leading: const Icon(Icons.group), | ||||
|               children: sharedAlbums.map((album) =>  | ||||
|                 AlbumThumbnailListTile( | ||||
|                   album: album, | ||||
|                   onTap: () => addToAlbum(album), | ||||
|                 ), | ||||
|               ).toList(), | ||||
|             ), | ||||
|             const SizedBox(height: 12), | ||||
|             ... albums.map((album) => | ||||
|               AlbumThumbnailListTile( | ||||
|                 album: album, | ||||
|                 onTap: () => addToAlbum(album), | ||||
|               ), | ||||
|             ).toList(), | ||||
|           ], | ||||
|         ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										115
									
								
								mobile/lib/modules/album/ui/album_thumbnail_listtile.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								mobile/lib/modules/album/ui/album_thumbnail_listtile.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:cached_network_image/cached_network_image.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hive/hive.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/utils/image_url_builder.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| class AlbumThumbnailListTile extends StatelessWidget { | ||||
|   const AlbumThumbnailListTile({ | ||||
|     Key? key, | ||||
|     required this.album, | ||||
|     this.onTap, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   final AlbumResponseDto album; | ||||
|   final void Function()? onTap; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     var box = Hive.box(userInfoBox); | ||||
|     var cardSize = 68.0; | ||||
|     var isDarkMode = Theme.of(context).brightness == Brightness.dark; | ||||
|  | ||||
|     buildEmptyThumbnail() { | ||||
|       return Container( | ||||
|         decoration: BoxDecoration( | ||||
|           color: isDarkMode ? Colors.grey[800] : Colors.grey[200], | ||||
|         ), | ||||
|         child: SizedBox( | ||||
|           height: cardSize, | ||||
|           width: cardSize, | ||||
|           child: const Center( | ||||
|             child: Icon(Icons.no_photography), | ||||
|           ), | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     buildAlbumThumbnail() { | ||||
|       return CachedNetworkImage( | ||||
|         width: cardSize, | ||||
|         height: cardSize, | ||||
|         fit: BoxFit.cover, | ||||
|         fadeInDuration: const Duration(milliseconds: 200), | ||||
|         imageUrl: getAlbumThumbnailUrl( | ||||
|           album, | ||||
|           type: ThumbnailFormat.JPEG, | ||||
|         ), | ||||
|         httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, | ||||
|         cacheKey: getAlbumThumbNailCacheKey(album, type: ThumbnailFormat.JPEG), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return GestureDetector( | ||||
|       behavior: HitTestBehavior.opaque, | ||||
|       onTap: onTap ?? () { | ||||
|         AutoRouter.of(context).push(AlbumViewerRoute(albumId: album.id)); | ||||
|       }, | ||||
|       child: Padding( | ||||
|         padding: const EdgeInsets.only(bottom: 12.0), | ||||
|         child: Row( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             ClipRRect( | ||||
|               borderRadius: BorderRadius.circular(8), | ||||
|               child: album.albumThumbnailAssetId == null | ||||
|                   ? buildEmptyThumbnail() | ||||
|                   : buildAlbumThumbnail(), | ||||
|             ), | ||||
|             Padding( | ||||
|               padding: const EdgeInsets.only( | ||||
|                 left: 8.0, | ||||
|                 right: 8.0, | ||||
|               ), | ||||
|               child: Column( | ||||
|                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                 children: [ | ||||
|                   Text( | ||||
|                     album.albumName, | ||||
|                     style: const TextStyle( | ||||
|                       fontWeight: FontWeight.bold, | ||||
|                     ), | ||||
|                   ), | ||||
|                   Row( | ||||
|                     mainAxisSize: MainAxisSize.min, | ||||
|                     children: [ | ||||
|                       Text( | ||||
|                         album.assetCount == 1 | ||||
|                             ? 'album_thumbnail_card_item' | ||||
|                             : 'album_thumbnail_card_items', | ||||
|                         style: const TextStyle( | ||||
|                           fontSize: 12, | ||||
|                         ), | ||||
|                       ).tr(args: ['${album.assetCount}']), | ||||
|                       if (album.shared) | ||||
|                         const Text( | ||||
|                           'album_thumbnail_card_shared', | ||||
|                           style: TextStyle( | ||||
|                             fontSize: 12, | ||||
|                           ), | ||||
|                         ).tr() | ||||
|                     ], | ||||
|                   ) | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -11,12 +11,18 @@ import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart | ||||
| import 'package:immich_mobile/modules/album/ui/album_title_text_field.dart'; | ||||
| import 'package:immich_mobile/modules/album/ui/shared_album_thumbnail_image.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/shared/models/asset.dart'; | ||||
|  | ||||
| // ignore: must_be_immutable | ||||
| class CreateAlbumPage extends HookConsumerWidget { | ||||
|   bool isSharedAlbum; | ||||
|   final bool isSharedAlbum; | ||||
|   final List<Asset>? initialAssets; | ||||
|  | ||||
|   CreateAlbumPage({Key? key, required this.isSharedAlbum}) : super(key: key); | ||||
|   const CreateAlbumPage({ | ||||
|     Key? key,  | ||||
|     required this.isSharedAlbum, | ||||
|     this.initialAssets, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|   | ||||
| @@ -11,6 +11,7 @@ class TopControlAppBar extends HookConsumerWidget with PreferredSizeWidget { | ||||
|     required this.onDownloadPressed, | ||||
|     required this.onSharePressed, | ||||
|     required this.onDeletePressed, | ||||
|     required this.onAddToAlbumPressed, | ||||
|     required this.onToggleMotionVideo, | ||||
|     required this.isPlayingMotionVideo, | ||||
|   }) : super(key: key); | ||||
| @@ -20,6 +21,7 @@ class TopControlAppBar extends HookConsumerWidget with PreferredSizeWidget { | ||||
|   final VoidCallback? onDownloadPressed; | ||||
|   final VoidCallback onToggleMotionVideo; | ||||
|   final VoidCallback onDeletePressed; | ||||
|   final VoidCallback onAddToAlbumPressed; | ||||
|   final Function onSharePressed; | ||||
|   final bool isPlayingMotionVideo; | ||||
|  | ||||
| @@ -80,6 +82,18 @@ class TopControlAppBar extends HookConsumerWidget with PreferredSizeWidget { | ||||
|             color: Colors.grey[200], | ||||
|           ), | ||||
|         ), | ||||
|         if (asset.isRemote) | ||||
|           IconButton( | ||||
|             iconSize: iconSize, | ||||
|             splashRadius: iconSize, | ||||
|             onPressed: () { | ||||
|               onAddToAlbumPressed(); | ||||
|             }, | ||||
|             icon: Icon( | ||||
|               Icons.add, | ||||
|               color: Colors.grey[200], | ||||
|             ), | ||||
|           ), | ||||
|         IconButton( | ||||
|           iconSize: iconSize, | ||||
|           splashRadius: iconSize, | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hive/hive.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/modules/album/ui/add_to_album_list.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart'; | ||||
| @@ -105,6 +106,22 @@ class GalleryViewerPage extends HookConsumerWidget { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     void addToAlbum(Asset addToAlbumAsset) { | ||||
|       showModalBottomSheet( | ||||
|         shape: RoundedRectangleBorder( | ||||
|           borderRadius: BorderRadius.circular(15.0), | ||||
|         ), | ||||
|         barrierColor: Colors.transparent, | ||||
|         backgroundColor: Colors.transparent, | ||||
|         context: context, | ||||
|         builder: (BuildContext _) { | ||||
|           return AddToAlbumList( | ||||
|             asset: addToAlbumAsset, | ||||
|           ); | ||||
|         }, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return Scaffold( | ||||
|       backgroundColor: Colors.black, | ||||
|       appBar: TopControlAppBar( | ||||
| @@ -130,6 +147,7 @@ class GalleryViewerPage extends HookConsumerWidget { | ||||
|           isPlayingMotionVideo.value = !isPlayingMotionVideo.value; | ||||
|         }), | ||||
|         onDeletePressed: () => handleDelete((assetList[indexOfAsset.value])), | ||||
|         onAddToAlbumPressed: () => addToAlbum(assetList[indexOfAsset.value]), | ||||
|       ), | ||||
|       body: SafeArea( | ||||
|         child: PageView.builder( | ||||
|   | ||||
| @@ -60,7 +60,8 @@ class _$AppRouter extends RootStackRouter { | ||||
|               isZoomedFunction: args.isZoomedFunction, | ||||
|               isZoomedListener: args.isZoomedListener, | ||||
|               loadPreview: args.loadPreview, | ||||
|               loadOriginal: args.loadOriginal)); | ||||
|               loadOriginal: args.loadOriginal, | ||||
|               showExifSheet: args.showExifSheet)); | ||||
|     }, | ||||
|     VideoViewerRoute.name: (routeData) { | ||||
|       final args = routeData.argsAs<VideoViewerRouteArgs>(); | ||||
| @@ -87,7 +88,9 @@ class _$AppRouter extends RootStackRouter { | ||||
|       return MaterialPageX<dynamic>( | ||||
|           routeData: routeData, | ||||
|           child: CreateAlbumPage( | ||||
|               key: args.key, isSharedAlbum: args.isSharedAlbum)); | ||||
|               key: args.key, | ||||
|               isSharedAlbum: args.isSharedAlbum, | ||||
|               initialAssets: args.initialAssets)); | ||||
|     }, | ||||
|     AssetSelectionRoute.name: (routeData) { | ||||
|       return CustomPage<AssetSelectionPageResult?>( | ||||
| @@ -307,7 +310,8 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> { | ||||
|       required void Function() isZoomedFunction, | ||||
|       required ValueNotifier<bool> isZoomedListener, | ||||
|       required bool loadPreview, | ||||
|       required bool loadOriginal}) | ||||
|       required bool loadOriginal, | ||||
|       void Function()? showExifSheet}) | ||||
|       : super(ImageViewerRoute.name, | ||||
|             path: '/image-viewer-page', | ||||
|             args: ImageViewerRouteArgs( | ||||
| @@ -318,7 +322,8 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> { | ||||
|                 isZoomedFunction: isZoomedFunction, | ||||
|                 isZoomedListener: isZoomedListener, | ||||
|                 loadPreview: loadPreview, | ||||
|                 loadOriginal: loadOriginal)); | ||||
|                 loadOriginal: loadOriginal, | ||||
|                 showExifSheet: showExifSheet)); | ||||
|  | ||||
|   static const String name = 'ImageViewerRoute'; | ||||
| } | ||||
| @@ -332,7 +337,8 @@ class ImageViewerRouteArgs { | ||||
|       required this.isZoomedFunction, | ||||
|       required this.isZoomedListener, | ||||
|       required this.loadPreview, | ||||
|       required this.loadOriginal}); | ||||
|       required this.loadOriginal, | ||||
|       this.showExifSheet}); | ||||
|  | ||||
|   final Key? key; | ||||
|  | ||||
| @@ -350,9 +356,11 @@ class ImageViewerRouteArgs { | ||||
|  | ||||
|   final bool loadOriginal; | ||||
|  | ||||
|   final void Function()? showExifSheet; | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'ImageViewerRouteArgs{key: $key, heroTag: $heroTag, asset: $asset, authToken: $authToken, isZoomedFunction: $isZoomedFunction, isZoomedListener: $isZoomedListener, loadPreview: $loadPreview, loadOriginal: $loadOriginal}'; | ||||
|     return 'ImageViewerRouteArgs{key: $key, heroTag: $heroTag, asset: $asset, authToken: $authToken, isZoomedFunction: $isZoomedFunction, isZoomedListener: $isZoomedListener, loadPreview: $loadPreview, loadOriginal: $loadOriginal, showExifSheet: $showExifSheet}'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -432,24 +440,31 @@ class SearchResultRouteArgs { | ||||
| /// generated route for | ||||
| /// [CreateAlbumPage] | ||||
| class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> { | ||||
|   CreateAlbumRoute({Key? key, required bool isSharedAlbum}) | ||||
|   CreateAlbumRoute( | ||||
|       {Key? key, required bool isSharedAlbum, List<Asset>? initialAssets}) | ||||
|       : super(CreateAlbumRoute.name, | ||||
|             path: '/create-album-page', | ||||
|             args: CreateAlbumRouteArgs(key: key, isSharedAlbum: isSharedAlbum)); | ||||
|             args: CreateAlbumRouteArgs( | ||||
|                 key: key, | ||||
|                 isSharedAlbum: isSharedAlbum, | ||||
|                 initialAssets: initialAssets)); | ||||
|  | ||||
|   static const String name = 'CreateAlbumRoute'; | ||||
| } | ||||
|  | ||||
| class CreateAlbumRouteArgs { | ||||
|   const CreateAlbumRouteArgs({this.key, required this.isSharedAlbum}); | ||||
|   const CreateAlbumRouteArgs( | ||||
|       {this.key, required this.isSharedAlbum, this.initialAssets}); | ||||
|  | ||||
|   final Key? key; | ||||
|  | ||||
|   final bool isSharedAlbum; | ||||
|  | ||||
|   final List<Asset>? initialAssets; | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'CreateAlbumRouteArgs{key: $key, isSharedAlbum: $isSharedAlbum}'; | ||||
|     return 'CreateAlbumRouteArgs{key: $key, isSharedAlbum: $isSharedAlbum, initialAssets: $initialAssets}'; | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user