You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	feat(mobile): Improve album UI and Interactions (#3754)
* fix: outlick editable field does not change edit icon * fix: unfocus on submit change album name * styling * styling * confirm dialog * Confirm deletion * render user * user avatar with image * use UserCircleAvatar * rights * stlying * remove/leave options * styling * state management --------- Co-authored-by: Alex Tran <Alex.Tran@conductix.com>
This commit is contained in:
		| @@ -300,5 +300,6 @@ | ||||
|   "version_announcement_overlay_text_1": "Hi friend, there is a new release of", | ||||
|   "version_announcement_overlay_text_2": "please take your time to visit the ", | ||||
|   "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", | ||||
|   "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89" | ||||
| } | ||||
|   "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", | ||||
|   "translated_text_options": "Options" | ||||
| } | ||||
|   | ||||
| @@ -56,6 +56,16 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> { | ||||
|     return _albumService.removeAssetFromAlbum(album, assets); | ||||
|   } | ||||
|  | ||||
|   Future<bool> removeUserFromAlbum(Album album, User user) async { | ||||
|     final result = await _albumService.removeUserFromAlbum(album, user); | ||||
|  | ||||
|     if (result && album.sharedUsers.isEmpty) { | ||||
|       state = state.where((element) => element.id != album.id).toList(); | ||||
|     } | ||||
|  | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _streamSub.cancel(); | ||||
|   | ||||
| @@ -348,6 +348,26 @@ class AlbumService { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<bool> removeUserFromAlbum( | ||||
|     Album album, | ||||
|     User user, | ||||
|   ) async { | ||||
|     try { | ||||
|       await _apiService.albumApi.removeUserFromAlbum( | ||||
|         album.remoteId!, | ||||
|         user.id, | ||||
|       ); | ||||
|  | ||||
|       album.sharedUsers.remove(user); | ||||
|       await _db.writeTxn(() => album.sharedUsers.update(unlink: [user])); | ||||
|  | ||||
|       return true; | ||||
|     } catch (e) { | ||||
|       debugPrint("Error removeUserFromAlbum  ${e.toString()}"); | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<bool> changeTitleAlbum( | ||||
|     Album album, | ||||
|     String newAlbumTitle, | ||||
|   | ||||
| @@ -69,6 +69,11 @@ class AlbumTitleTextField extends ConsumerWidget { | ||||
|           borderRadius: BorderRadius.circular(10), | ||||
|         ), | ||||
|         hintText: 'share_add_title'.tr(), | ||||
|         hintStyle: TextStyle( | ||||
|           fontSize: 28, | ||||
|           color: isDarkTheme ? Colors.grey[300] : Colors.grey[700], | ||||
|           fontWeight: FontWeight.bold, | ||||
|         ), | ||||
|         focusColor: Colors.grey[300], | ||||
|         fillColor: isDarkTheme | ||||
|             ? const Color.fromARGB(255, 32, 33, 35) | ||||
|   | ||||
| @@ -39,7 +39,7 @@ class AlbumViewerAppbar extends HookConsumerWidget | ||||
|     final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText; | ||||
|     final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum; | ||||
|  | ||||
|     void onDeleteAlbumPressed() async { | ||||
|     deleteAlbum() async { | ||||
|       ImmichLoadingOverlayController.appLoader.show(); | ||||
|  | ||||
|       final bool success; | ||||
| @@ -65,6 +65,52 @@ class AlbumViewerAppbar extends HookConsumerWidget | ||||
|       ImmichLoadingOverlayController.appLoader.hide(); | ||||
|     } | ||||
|  | ||||
|     Future<void> showConfirmationDialog() async { | ||||
|       return showDialog<void>( | ||||
|         context: context, | ||||
|         barrierDismissible: false, // user must tap button! | ||||
|         builder: (BuildContext context) { | ||||
|           return AlertDialog( | ||||
|             title: const Text('Delete album'), | ||||
|             content: const Text( | ||||
|               'Are you sure you want to delete this album from your account?', | ||||
|             ), | ||||
|             actions: <Widget>[ | ||||
|               TextButton( | ||||
|                 onPressed: () => Navigator.pop(context, 'Cancel'), | ||||
|                 child: Text( | ||||
|                   'Cancel', | ||||
|                   style: TextStyle( | ||||
|                     color: Theme.of(context).primaryColor, | ||||
|                     fontWeight: FontWeight.bold, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|               TextButton( | ||||
|                 onPressed: () { | ||||
|                   Navigator.pop(context, 'Confirm'); | ||||
|                   deleteAlbum(); | ||||
|                 }, | ||||
|                 child: Text( | ||||
|                   'Confirm', | ||||
|                   style: TextStyle( | ||||
|                     fontWeight: FontWeight.bold, | ||||
|                     color: Theme.of(context).brightness == Brightness.light | ||||
|                         ? Colors.red | ||||
|                         : Colors.red[300], | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ); | ||||
|         }, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     void onDeleteAlbumPressed() async { | ||||
|       showConfirmationDialog(); | ||||
|     } | ||||
|  | ||||
|     void onLeaveAlbumPressed() async { | ||||
|       ImmichLoadingOverlayController.appLoader.show(); | ||||
|  | ||||
| @@ -152,43 +198,61 @@ class AlbumViewerAppbar extends HookConsumerWidget | ||||
|     } | ||||
|  | ||||
|     void buildBottomSheet() { | ||||
|       final ownerActions = [ | ||||
|         ListTile( | ||||
|           leading: const Icon(Icons.person_add_alt_rounded), | ||||
|           onTap: () { | ||||
|             Navigator.pop(context); | ||||
|             onAddUsers!(album); | ||||
|           }, | ||||
|           title: const Text( | ||||
|             "album_viewer_page_share_add_users", | ||||
|             style: TextStyle(fontWeight: FontWeight.bold), | ||||
|           ).tr(), | ||||
|         ), | ||||
|         ListTile( | ||||
|           leading: const Icon(Icons.settings_rounded), | ||||
|           onTap: () => | ||||
|               AutoRouter.of(context).navigate(AlbumOptionsRoute(album: album)), | ||||
|           title: const Text( | ||||
|             "translated_text_options", | ||||
|             style: TextStyle(fontWeight: FontWeight.bold), | ||||
|           ).tr(), | ||||
|         ), | ||||
|       ]; | ||||
|  | ||||
|       final commonActions = [ | ||||
|         ListTile( | ||||
|           leading: const Icon(Icons.add_photo_alternate_outlined), | ||||
|           onTap: () { | ||||
|             Navigator.pop(context); | ||||
|             onAddPhotos!(album); | ||||
|           }, | ||||
|           title: const Text( | ||||
|             "share_add_photos", | ||||
|             style: TextStyle(fontWeight: FontWeight.bold), | ||||
|           ).tr(), | ||||
|         ), | ||||
|       ]; | ||||
|       showModalBottomSheet( | ||||
|         backgroundColor: Theme.of(context).scaffoldBackgroundColor, | ||||
|         isScrollControlled: false, | ||||
|         context: context, | ||||
|         builder: (context) { | ||||
|           return SafeArea( | ||||
|             child: Column( | ||||
|               mainAxisSize: MainAxisSize.min, | ||||
|               children: [ | ||||
|                 buildBottomSheetActionButton(), | ||||
|                 if (selected.isEmpty && onAddPhotos != null) | ||||
|                   ListTile( | ||||
|                     leading: const Icon(Icons.add_photo_alternate_outlined), | ||||
|                     onTap: () { | ||||
|                       Navigator.pop(context); | ||||
|                       onAddPhotos!(album); | ||||
|                     }, | ||||
|                     title: const Text( | ||||
|                       "share_add_photos", | ||||
|                       style: TextStyle(fontWeight: FontWeight.bold), | ||||
|                     ).tr(), | ||||
|                   ), | ||||
|                 if (selected.isEmpty && | ||||
|                     onAddPhotos != null && | ||||
|                     userId == album.ownerId) | ||||
|                   ListTile( | ||||
|                     leading: const Icon(Icons.person_add_alt_rounded), | ||||
|                     onTap: () { | ||||
|                       Navigator.pop(context); | ||||
|                       onAddUsers!(album); | ||||
|                     }, | ||||
|                     title: const Text( | ||||
|                       "album_viewer_page_share_add_users", | ||||
|                       style: TextStyle(fontWeight: FontWeight.bold), | ||||
|                     ).tr(), | ||||
|                   ), | ||||
|               ], | ||||
|             child: Padding( | ||||
|               padding: const EdgeInsets.only(top: 24.0), | ||||
|               child: Column( | ||||
|                 mainAxisSize: MainAxisSize.min, | ||||
|                 children: [ | ||||
|                   buildBottomSheetActionButton(), | ||||
|                   if (selected.isEmpty && onAddPhotos != null) ...commonActions, | ||||
|                   if (selected.isEmpty && | ||||
|                       onAddPhotos != null && | ||||
|                       userId == album.ownerId) | ||||
|                     ...ownerActions | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|           ); | ||||
|         }, | ||||
| @@ -217,6 +281,8 @@ class AlbumViewerAppbar extends HookConsumerWidget | ||||
|                 toastType: ToastType.error, | ||||
|               ); | ||||
|             } | ||||
|  | ||||
|             titleFocusNode.unfocus(); | ||||
|           }, | ||||
|           icon: const Icon(Icons.check_rounded), | ||||
|           splashRadius: 25, | ||||
|   | ||||
| @@ -84,6 +84,11 @@ class AlbumViewerEditableTitle extends HookConsumerWidget { | ||||
|             : Colors.grey[200], | ||||
|         filled: titleFocusNode.hasFocus, | ||||
|         hintText: 'share_add_title'.tr(), | ||||
|         hintStyle: TextStyle( | ||||
|           fontSize: 28, | ||||
|           color: isDarkTheme ? Colors.grey[300] : Colors.grey[700], | ||||
|           fontWeight: FontWeight.bold, | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
							
								
								
									
										205
									
								
								mobile/lib/modules/album/views/album_options_part.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								mobile/lib/modules/album/views/album_options_part.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,205 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.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/modules/album/providers/shared_album.provider.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/shared/models/album.dart'; | ||||
| import 'package:immich_mobile/shared/models/user.dart'; | ||||
| import 'package:immich_mobile/shared/ui/immich_toast.dart'; | ||||
| import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; | ||||
| import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; | ||||
|  | ||||
| class AlbumOptionsPage extends HookConsumerWidget { | ||||
|   final Album album; | ||||
|  | ||||
|   const AlbumOptionsPage({super.key, required this.album}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final sharedUsers = useState(album.sharedUsers.toList()); | ||||
|     final owner = album.owner.value; | ||||
|     final userId = ref.watch(authenticationProvider).userId; | ||||
|     final isOwner = owner?.id == userId; | ||||
|  | ||||
|     void showErrorMessage() { | ||||
|       Navigator.pop(context); | ||||
|       ImmichToast.show( | ||||
|         context: context, | ||||
|         msg: "Error leaving/removing from album", | ||||
|         toastType: ToastType.error, | ||||
|         gravity: ToastGravity.BOTTOM, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     void leaveAlbum() async { | ||||
|       ImmichLoadingOverlayController.appLoader.show(); | ||||
|  | ||||
|       try { | ||||
|         final isSuccess = | ||||
|             await ref.read(sharedAlbumProvider.notifier).leaveAlbum(album); | ||||
|  | ||||
|         if (isSuccess) { | ||||
|           AutoRouter.of(context) | ||||
|               .navigate(const TabControllerRoute(children: [SharingRoute()])); | ||||
|         } else { | ||||
|           showErrorMessage(); | ||||
|         } | ||||
|       } catch (_) { | ||||
|         showErrorMessage(); | ||||
|       } | ||||
|  | ||||
|       ImmichLoadingOverlayController.appLoader.hide(); | ||||
|     } | ||||
|  | ||||
|     void removeUserFromAlbum(User user) async { | ||||
|       ImmichLoadingOverlayController.appLoader.show(); | ||||
|  | ||||
|       try { | ||||
|         await ref | ||||
|             .read(sharedAlbumProvider.notifier) | ||||
|             .removeUserFromAlbum(album, user); | ||||
|         album.sharedUsers.remove(user); | ||||
|         sharedUsers.value = album.sharedUsers.toList(); | ||||
|       } catch (error) { | ||||
|         showErrorMessage(); | ||||
|       } | ||||
|  | ||||
|       Navigator.pop(context); | ||||
|       ImmichLoadingOverlayController.appLoader.hide(); | ||||
|     } | ||||
|  | ||||
|     void handleUserClick(User user) { | ||||
|       var actions = []; | ||||
|  | ||||
|       if (user.id == userId) { | ||||
|         actions = [ | ||||
|           ListTile( | ||||
|             leading: const Icon(Icons.exit_to_app_rounded), | ||||
|             title: const Text("Leave album"), | ||||
|             onTap: leaveAlbum, | ||||
|           ), | ||||
|         ]; | ||||
|       } | ||||
|  | ||||
|       if (isOwner) { | ||||
|         actions = [ | ||||
|           ListTile( | ||||
|             leading: const Icon(Icons.person_remove_rounded), | ||||
|             title: const Text("Remove user from album"), | ||||
|             onTap: () => removeUserFromAlbum(user), | ||||
|           ), | ||||
|         ]; | ||||
|       } | ||||
|  | ||||
|       showModalBottomSheet( | ||||
|         backgroundColor: Theme.of(context).scaffoldBackgroundColor, | ||||
|         isScrollControlled: false, | ||||
|         context: context, | ||||
|         builder: (context) { | ||||
|           return SafeArea( | ||||
|             child: Padding( | ||||
|               padding: const EdgeInsets.only(top: 24.0), | ||||
|               child: Column( | ||||
|                 mainAxisSize: MainAxisSize.min, | ||||
|                 children: [...actions], | ||||
|               ), | ||||
|             ), | ||||
|           ); | ||||
|         }, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     buildOwnerInfo() { | ||||
|       return ListTile( | ||||
|         leading: owner != null | ||||
|             ? UserCircleAvatar( | ||||
|                 user: owner, | ||||
|                 useRandomBackgroundColor: true, | ||||
|               ) | ||||
|             : const SizedBox(), | ||||
|         title: Text( | ||||
|           album.owner.value?.firstName ?? "", | ||||
|           style: const TextStyle( | ||||
|             fontWeight: FontWeight.bold, | ||||
|           ), | ||||
|         ), | ||||
|         subtitle: Text( | ||||
|           album.owner.value?.email ?? "", | ||||
|           style: TextStyle(color: Colors.grey[500]), | ||||
|         ), | ||||
|         trailing: const Text( | ||||
|           "Owner", | ||||
|           style: TextStyle( | ||||
|             fontWeight: FontWeight.bold, | ||||
|           ), | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     buildSharedUsersList() { | ||||
|       return ListView.builder( | ||||
|         shrinkWrap: true, | ||||
|         itemCount: sharedUsers.value.length, | ||||
|         itemBuilder: (context, index) { | ||||
|           final user = sharedUsers.value[index]; | ||||
|           return ListTile( | ||||
|             leading: UserCircleAvatar( | ||||
|               user: user, | ||||
|               useRandomBackgroundColor: true, | ||||
|               radius: 22, | ||||
|             ), | ||||
|             title: Text( | ||||
|               user.firstName, | ||||
|               style: const TextStyle( | ||||
|                 fontWeight: FontWeight.bold, | ||||
|               ), | ||||
|             ), | ||||
|             subtitle: Text( | ||||
|               user.email, | ||||
|               style: TextStyle(color: Colors.grey[500]), | ||||
|             ), | ||||
|             trailing: userId == user.id || isOwner | ||||
|                 ? const Icon(Icons.more_horiz_rounded) | ||||
|                 : const SizedBox(), | ||||
|             onTap: userId == user.id || isOwner | ||||
|                 ? () => handleUserClick(user) | ||||
|                 : null, | ||||
|           ); | ||||
|         }, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     buildSectionTitle(String text) { | ||||
|       return Padding( | ||||
|         padding: const EdgeInsets.all(16.0), | ||||
|         child: Text(text, style: Theme.of(context).textTheme.bodySmall), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: IconButton( | ||||
|           icon: const Icon(Icons.arrow_back_ios_new_rounded), | ||||
|           onPressed: () { | ||||
|             AutoRouter.of(context).pop(null); | ||||
|           }, | ||||
|         ), | ||||
|         centerTitle: true, | ||||
|         title: Text("translated_text_options".tr()), | ||||
|       ), | ||||
|       body: Column( | ||||
|         mainAxisAlignment: MainAxisAlignment.start, | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           buildSectionTitle("PEOPLE"), | ||||
|           buildOwnerInfo(), | ||||
|           buildSharedUsersList(), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -17,6 +17,7 @@ import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/shared/models/album.dart'; | ||||
| import 'package:immich_mobile/shared/models/asset.dart'; | ||||
| import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; | ||||
| import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; | ||||
| import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; | ||||
|  | ||||
| class AlbumViewerPage extends HookConsumerWidget { | ||||
| @@ -116,7 +117,7 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|  | ||||
|     Widget buildControlButton(Album album) { | ||||
|       return Padding( | ||||
|         padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 8), | ||||
|         padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 16), | ||||
|         child: SizedBox( | ||||
|           height: 40, | ||||
|           child: ListView( | ||||
| @@ -141,7 +142,7 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|  | ||||
|     Widget buildTitle(Album album) { | ||||
|       return Padding( | ||||
|         padding: const EdgeInsets.only(left: 8, right: 8, top: 16), | ||||
|         padding: const EdgeInsets.only(left: 8, right: 8, top: 24), | ||||
|         child: userId == album.ownerId && album.isRemote | ||||
|             ? AlbumViewerEditableTitle( | ||||
|                 album: album, | ||||
| @@ -172,7 +173,6 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|       return Padding( | ||||
|         padding: EdgeInsets.only( | ||||
|           left: 16.0, | ||||
|           top: 8.0, | ||||
|           bottom: album.shared ? 0.0 : 8.0, | ||||
|         ), | ||||
|         child: Text( | ||||
| @@ -180,7 +180,34 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|           style: const TextStyle( | ||||
|             fontSize: 14, | ||||
|             fontWeight: FontWeight.bold, | ||||
|             color: Colors.grey, | ||||
|           ), | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     Widget buildSharedUserIconsRow(Album album) { | ||||
|       return GestureDetector( | ||||
|         onTap: () async { | ||||
|           await AutoRouter.of(context).push(AlbumOptionsRoute(album: album)); | ||||
|           ref.invalidate(albumDetailProvider(album.id)); | ||||
|         }, | ||||
|         child: SizedBox( | ||||
|           height: 50, | ||||
|           child: ListView.builder( | ||||
|             padding: const EdgeInsets.only(left: 16), | ||||
|             scrollDirection: Axis.horizontal, | ||||
|             itemBuilder: ((context, index) { | ||||
|               return Padding( | ||||
|                 padding: const EdgeInsets.only(right: 8.0), | ||||
|                 child: UserCircleAvatar( | ||||
|                   user: album.sharedUsers.toList()[index], | ||||
|                   radius: 18, | ||||
|                   size: 36, | ||||
|                   useRandomBackgroundColor: true, | ||||
|                 ), | ||||
|               ); | ||||
|             }), | ||||
|             itemCount: album.sharedUsers.length, | ||||
|           ), | ||||
|         ), | ||||
|       ); | ||||
| @@ -193,33 +220,7 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|         children: [ | ||||
|           buildTitle(album), | ||||
|           if (album.assets.isNotEmpty == true) buildAlbumDateRange(album), | ||||
|           if (album.shared) | ||||
|             SizedBox( | ||||
|               height: 50, | ||||
|               child: ListView.builder( | ||||
|                 padding: const EdgeInsets.only(left: 16), | ||||
|                 scrollDirection: Axis.horizontal, | ||||
|                 itemBuilder: ((context, index) { | ||||
|                   return Padding( | ||||
|                     padding: const EdgeInsets.only(right: 8.0), | ||||
|                     child: CircleAvatar( | ||||
|                       backgroundColor: Colors.grey[300], | ||||
|                       radius: 18, | ||||
|                       child: Padding( | ||||
|                         padding: const EdgeInsets.all(2.0), | ||||
|                         child: ClipRRect( | ||||
|                           borderRadius: BorderRadius.circular(50.0), | ||||
|                           child: Image.asset( | ||||
|                             'assets/immich-logo-no-outline.png', | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ); | ||||
|                 }), | ||||
|                 itemCount: album.sharedUsers.length, | ||||
|               ), | ||||
|             ), | ||||
|           if (album.shared) buildSharedUserIconsRow(album), | ||||
|         ], | ||||
|       ); | ||||
|     } | ||||
|   | ||||
| @@ -73,9 +73,12 @@ class AssetSelectionPage extends HookConsumerWidget { | ||||
|                 AutoRouter.of(context) | ||||
|                     .popForced<AssetSelectionPageResult>(payload); | ||||
|               }, | ||||
|               child: const Text( | ||||
|               child: Text( | ||||
|                 "share_add", | ||||
|                 style: TextStyle(fontWeight: FontWeight.bold), | ||||
|                 style: TextStyle( | ||||
|                   fontWeight: FontWeight.bold, | ||||
|                   color: Theme.of(context).primaryColor, | ||||
|                 ), | ||||
|               ).tr(), | ||||
|             ), | ||||
|         ], | ||||
|   | ||||
| @@ -30,7 +30,8 @@ class CreateAlbumPage extends HookConsumerWidget { | ||||
|     final albumTitleTextFieldFocusNode = useFocusNode(); | ||||
|     final isAlbumTitleTextFieldFocus = useState(false); | ||||
|     final isAlbumTitleEmpty = useState(true); | ||||
|     final selectedAssets = useState<Set<Asset>>(initialAssets != null ? Set.from(initialAssets!) : const {}); | ||||
|     final selectedAssets = useState<Set<Asset>>( | ||||
|         initialAssets != null ? Set.from(initialAssets!) : const {},); | ||||
|     final isDarkTheme = Theme.of(context).brightness == Brightness.dark; | ||||
|  | ||||
|     showSelectUserPage() async { | ||||
| @@ -248,8 +249,9 @@ class CreateAlbumPage extends HookConsumerWidget { | ||||
|                   : null, | ||||
|               child: Text( | ||||
|                 'create_shared_album_page_create'.tr(), | ||||
|                 style: const TextStyle( | ||||
|                 style: TextStyle( | ||||
|                   fontWeight: FontWeight.bold, | ||||
|                   color: Theme.of(context).primaryColor, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import 'package:immich_mobile/modules/album/providers/suggested_shared_users.pro | ||||
| import 'package:immich_mobile/shared/models/album.dart'; | ||||
| import 'package:immich_mobile/shared/models/user.dart'; | ||||
| import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; | ||||
| import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; | ||||
|  | ||||
| class SelectAdditionalUserForSharingPage extends HookConsumerWidget { | ||||
|   final Album album; | ||||
| @@ -35,10 +36,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { | ||||
|           ), | ||||
|         ); | ||||
|       } else { | ||||
|         return CircleAvatar( | ||||
|           backgroundImage: | ||||
|               const AssetImage('assets/immich-logo-no-outline.png'), | ||||
|           backgroundColor: Theme.of(context).primaryColor.withAlpha(50), | ||||
|         return UserCircleAvatar( | ||||
|           user: user, | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/shared/models/asset.dart'; | ||||
| import 'package:immich_mobile/shared/models/user.dart'; | ||||
| import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; | ||||
| import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; | ||||
|  | ||||
| class SelectUserForSharingPage extends HookConsumerWidget { | ||||
|   const SelectUserForSharingPage({Key? key, required this.assets}) | ||||
| @@ -56,10 +57,8 @@ class SelectUserForSharingPage extends HookConsumerWidget { | ||||
|           ), | ||||
|         ); | ||||
|       } else { | ||||
|         return CircleAvatar( | ||||
|           backgroundImage: | ||||
|               const AssetImage('assets/immich-logo-no-outline.png'), | ||||
|           backgroundColor: Theme.of(context).primaryColor.withAlpha(50), | ||||
|         return UserCircleAvatar( | ||||
|           user: user, | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/user_circle_avatar.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; | ||||
| import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||
|  | ||||
| @@ -29,7 +30,7 @@ class HomePageAppBar extends ConsumerWidget implements PreferredSizeWidget { | ||||
|         backupState.backgroundBackup || backupState.autoBackup; | ||||
|     final ServerInfoState serverInfoState = ref.watch(serverInfoProvider); | ||||
|     AuthenticationState authState = ref.watch(authenticationProvider); | ||||
|  | ||||
|     final user = Store.get(StoreKey.currentUser); | ||||
|     buildProfilePhoto() { | ||||
|       if (authState.profileImagePath.isEmpty) { | ||||
|         return IconButton( | ||||
| @@ -47,9 +48,10 @@ class HomePageAppBar extends ConsumerWidget implements PreferredSizeWidget { | ||||
|           onTap: () { | ||||
|             Scaffold.of(context).openDrawer(); | ||||
|           }, | ||||
|           child: const UserCircleAvatar( | ||||
|           child: UserCircleAvatar( | ||||
|             radius: 18, | ||||
|             size: 33, | ||||
|             user: user, | ||||
|           ), | ||||
|         ); | ||||
|       } | ||||
|   | ||||
| @@ -3,7 +3,8 @@ import 'package:flutter_hooks/flutter_hooks.dart' hide Store; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:image_picker/image_picker.dart'; | ||||
| import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/user_circle_avatar.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; | ||||
| import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||
| import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; | ||||
| @@ -19,11 +20,13 @@ class ProfileDrawerHeader extends HookConsumerWidget { | ||||
|     final uploadProfileImageStatus = | ||||
|         ref.watch(uploadProfileImageProvider).status; | ||||
|     final isDarkMode = Theme.of(context).brightness == Brightness.dark; | ||||
|     final user = Store.get(StoreKey.currentUser); | ||||
|  | ||||
|     buildUserProfileImage() { | ||||
|       var userImage = const UserCircleAvatar( | ||||
|       var userImage = UserCircleAvatar( | ||||
|         radius: 35, | ||||
|         size: 66, | ||||
|         user: user, | ||||
|       ); | ||||
|  | ||||
|       if (authState.profileImagePath.isEmpty) { | ||||
|   | ||||
| @@ -1,44 +0,0 @@ | ||||
| import 'dart:math'; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/ui/transparent_image.dart'; | ||||
|  | ||||
| class UserCircleAvatar extends ConsumerWidget { | ||||
|   final double radius; | ||||
|   final double size; | ||||
|   const UserCircleAvatar({super.key, required this.radius, required this.size}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     AuthenticationState authState = ref.watch(authenticationProvider); | ||||
|  | ||||
|     var profileImageUrl = | ||||
|         '${Store.get(StoreKey.serverEndpoint)}/user/profile-image/${authState.userId}?d=${Random().nextInt(1024)}'; | ||||
|     return CircleAvatar( | ||||
|       backgroundColor: Theme.of(context).primaryColor, | ||||
|       radius: radius, | ||||
|       child: ClipRRect( | ||||
|         borderRadius: BorderRadius.circular(50), | ||||
|         child: FadeInImage( | ||||
|           fit: BoxFit.cover, | ||||
|           placeholder: MemoryImage(kTransparentImage), | ||||
|           width: size, | ||||
|           height: size, | ||||
|           image: NetworkImage( | ||||
|             profileImageUrl, | ||||
|             headers: { | ||||
|               "Authorization": "Bearer ${Store.get(StoreKey.accessToken)}" | ||||
|             }, | ||||
|           ), | ||||
|           fadeInDuration: const Duration(milliseconds: 200), | ||||
|           imageErrorBuilder: (context, error, stackTrace) => | ||||
|               Image.memory(kTransparentImage), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart'; | ||||
| import 'package:immich_mobile/modules/album/views/album_options_part.dart'; | ||||
| import 'package:immich_mobile/modules/album/views/album_viewer_page.dart'; | ||||
| import 'package:immich_mobile/modules/album/views/asset_selection_page.dart'; | ||||
| import 'package:immich_mobile/modules/album/views/create_album_page.dart'; | ||||
| @@ -152,6 +153,7 @@ part 'router.gr.dart'; | ||||
|     ), | ||||
|     AutoRoute(page: AllPeoplePage, guards: [AuthGuard, DuplicateGuard]), | ||||
|     AutoRoute(page: MemoryPage, guards: [AuthGuard, DuplicateGuard]), | ||||
|     AutoRoute(page: AlbumOptionsPage, guards: [AuthGuard, DuplicateGuard]), | ||||
|   ], | ||||
| ) | ||||
| class AppRouter extends _$AppRouter { | ||||
|   | ||||
| @@ -296,6 +296,16 @@ class _$AppRouter extends RootStackRouter { | ||||
|         ), | ||||
|       ); | ||||
|     }, | ||||
|     AlbumOptionsRoute.name: (routeData) { | ||||
|       final args = routeData.argsAs<AlbumOptionsRouteArgs>(); | ||||
|       return MaterialPageX<dynamic>( | ||||
|         routeData: routeData, | ||||
|         child: AlbumOptionsPage( | ||||
|           key: args.key, | ||||
|           album: args.album, | ||||
|         ), | ||||
|       ); | ||||
|     }, | ||||
|     HomeRoute.name: (routeData) { | ||||
|       return MaterialPageX<dynamic>( | ||||
|         routeData: routeData, | ||||
| @@ -595,6 +605,14 @@ class _$AppRouter extends RootStackRouter { | ||||
|             duplicateGuard, | ||||
|           ], | ||||
|         ), | ||||
|         RouteConfig( | ||||
|           AlbumOptionsRoute.name, | ||||
|           path: '/album-options-page', | ||||
|           guards: [ | ||||
|             authGuard, | ||||
|             duplicateGuard, | ||||
|           ], | ||||
|         ), | ||||
|       ]; | ||||
| } | ||||
|  | ||||
| @@ -1319,6 +1337,40 @@ class MemoryRouteArgs { | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// generated route for | ||||
| /// [AlbumOptionsPage] | ||||
| class AlbumOptionsRoute extends PageRouteInfo<AlbumOptionsRouteArgs> { | ||||
|   AlbumOptionsRoute({ | ||||
|     Key? key, | ||||
|     required Album album, | ||||
|   }) : super( | ||||
|           AlbumOptionsRoute.name, | ||||
|           path: '/album-options-page', | ||||
|           args: AlbumOptionsRouteArgs( | ||||
|             key: key, | ||||
|             album: album, | ||||
|           ), | ||||
|         ); | ||||
|  | ||||
|   static const String name = 'AlbumOptionsRoute'; | ||||
| } | ||||
|  | ||||
| class AlbumOptionsRouteArgs { | ||||
|   const AlbumOptionsRouteArgs({ | ||||
|     this.key, | ||||
|     required this.album, | ||||
|   }); | ||||
|  | ||||
|   final Key? key; | ||||
|  | ||||
|   final Album album; | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'AlbumOptionsRouteArgs{key: $key, album: $album}'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// generated route for | ||||
| /// [HomePage] | ||||
| class HomeRoute extends PageRouteInfo<void> { | ||||
|   | ||||
							
								
								
									
										75
									
								
								mobile/lib/shared/ui/user_circle_avatar.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								mobile/lib/shared/ui/user_circle_avatar.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| import 'dart:math'; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/models/user.dart'; | ||||
| import 'package:immich_mobile/shared/ui/transparent_image.dart'; | ||||
|  | ||||
| // ignore: must_be_immutable | ||||
| class UserCircleAvatar extends ConsumerWidget { | ||||
|   final User user; | ||||
|   double radius; | ||||
|   double size; | ||||
|   bool useRandomBackgroundColor; | ||||
|  | ||||
|   UserCircleAvatar({ | ||||
|     super.key, | ||||
|     this.radius = 22, | ||||
|     this.size = 44, | ||||
|     this.useRandomBackgroundColor = false, | ||||
|     required this.user, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final randomColors = [ | ||||
|       Colors.red[200], | ||||
|       Colors.blue[200], | ||||
|       Colors.green[200], | ||||
|       Colors.yellow[200], | ||||
|       Colors.purple[200], | ||||
|       Colors.orange[200], | ||||
|       Colors.pink[200], | ||||
|       Colors.teal[200], | ||||
|       Colors.indigo[200], | ||||
|       Colors.cyan[200], | ||||
|       Colors.brown[200], | ||||
|     ]; | ||||
|  | ||||
|     final profileImageUrl = | ||||
|         '${Store.get(StoreKey.serverEndpoint)}/user/profile-image/${user.id}?d=${Random().nextInt(1024)}'; | ||||
|     return CircleAvatar( | ||||
|       backgroundColor: useRandomBackgroundColor | ||||
|           ? randomColors[Random().nextInt(randomColors.length)] | ||||
|           : Theme.of(context).primaryColor, | ||||
|       radius: radius, | ||||
|       child: user.profileImagePath == "" | ||||
|           ? Text( | ||||
|               user.firstName[0], | ||||
|               style: const TextStyle( | ||||
|                 fontWeight: FontWeight.bold, | ||||
|                 color: Colors.black, | ||||
|               ), | ||||
|             ) | ||||
|           : ClipRRect( | ||||
|               borderRadius: BorderRadius.circular(50), | ||||
|               child: FadeInImage( | ||||
|                 fit: BoxFit.cover, | ||||
|                 placeholder: MemoryImage(kTransparentImage), | ||||
|                 width: size, | ||||
|                 height: size, | ||||
|                 image: NetworkImage( | ||||
|                   profileImageUrl, | ||||
|                   headers: { | ||||
|                     "Authorization": "Bearer ${Store.get(StoreKey.accessToken)}" | ||||
|                   }, | ||||
|                 ), | ||||
|                 fadeInDuration: const Duration(milliseconds: 200), | ||||
|                 imageErrorBuilder: (context, error, stackTrace) => | ||||
|                     Image.memory(kTransparentImage), | ||||
|               ), | ||||
|             ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user