diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 8aeae7043..3082febe1 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -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" -} \ No newline at end of file + "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", + "translated_text_options": "Options" +} diff --git a/mobile/lib/modules/album/providers/shared_album.provider.dart b/mobile/lib/modules/album/providers/shared_album.provider.dart index 8b342dd45..4f36c4633 100644 --- a/mobile/lib/modules/album/providers/shared_album.provider.dart +++ b/mobile/lib/modules/album/providers/shared_album.provider.dart @@ -56,6 +56,16 @@ class SharedAlbumNotifier extends StateNotifier> { return _albumService.removeAssetFromAlbum(album, assets); } + Future 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(); diff --git a/mobile/lib/modules/album/services/album.service.dart b/mobile/lib/modules/album/services/album.service.dart index 0960978a4..4488eca23 100644 --- a/mobile/lib/modules/album/services/album.service.dart +++ b/mobile/lib/modules/album/services/album.service.dart @@ -348,6 +348,26 @@ class AlbumService { } } + Future 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 changeTitleAlbum( Album album, String newAlbumTitle, diff --git a/mobile/lib/modules/album/ui/album_title_text_field.dart b/mobile/lib/modules/album/ui/album_title_text_field.dart index 8e63506b8..38c1f681a 100644 --- a/mobile/lib/modules/album/ui/album_title_text_field.dart +++ b/mobile/lib/modules/album/ui/album_title_text_field.dart @@ -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) diff --git a/mobile/lib/modules/album/ui/album_viewer_appbar.dart b/mobile/lib/modules/album/ui/album_viewer_appbar.dart index 3392ed518..c3cfa0a0a 100644 --- a/mobile/lib/modules/album/ui/album_viewer_appbar.dart +++ b/mobile/lib/modules/album/ui/album_viewer_appbar.dart @@ -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 showConfirmationDialog() async { + return showDialog( + 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: [ + 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, diff --git a/mobile/lib/modules/album/ui/album_viewer_editable_title.dart b/mobile/lib/modules/album/ui/album_viewer_editable_title.dart index b7a5d3544..8a7e46f8c 100644 --- a/mobile/lib/modules/album/ui/album_viewer_editable_title.dart +++ b/mobile/lib/modules/album/ui/album_viewer_editable_title.dart @@ -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, + ), ), ); } diff --git a/mobile/lib/modules/album/views/album_options_part.dart b/mobile/lib/modules/album/views/album_options_part.dart new file mode 100644 index 000000000..eb08b6bda --- /dev/null +++ b/mobile/lib/modules/album/views/album_options_part.dart @@ -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(), + ], + ), + ); + } +} diff --git a/mobile/lib/modules/album/views/album_viewer_page.dart b/mobile/lib/modules/album/views/album_viewer_page.dart index 31c48c450..cb389059a 100644 --- a/mobile/lib/modules/album/views/album_viewer_page.dart +++ b/mobile/lib/modules/album/views/album_viewer_page.dart @@ -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), ], ); } diff --git a/mobile/lib/modules/album/views/asset_selection_page.dart b/mobile/lib/modules/album/views/asset_selection_page.dart index 7ea60e249..afec5f8ae 100644 --- a/mobile/lib/modules/album/views/asset_selection_page.dart +++ b/mobile/lib/modules/album/views/asset_selection_page.dart @@ -73,9 +73,12 @@ class AssetSelectionPage extends HookConsumerWidget { AutoRouter.of(context) .popForced(payload); }, - child: const Text( + child: Text( "share_add", - style: TextStyle(fontWeight: FontWeight.bold), + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor, + ), ).tr(), ), ], diff --git a/mobile/lib/modules/album/views/create_album_page.dart b/mobile/lib/modules/album/views/create_album_page.dart index 4a4e7a9e5..e1f0d65e7 100644 --- a/mobile/lib/modules/album/views/create_album_page.dart +++ b/mobile/lib/modules/album/views/create_album_page.dart @@ -30,7 +30,8 @@ class CreateAlbumPage extends HookConsumerWidget { final albumTitleTextFieldFocusNode = useFocusNode(); final isAlbumTitleTextFieldFocus = useState(false); final isAlbumTitleEmpty = useState(true); - final selectedAssets = useState>(initialAssets != null ? Set.from(initialAssets!) : const {}); + final selectedAssets = useState>( + 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, ), ), ), diff --git a/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart b/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart index ec7ddb17e..e1adfa551 100644 --- a/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart +++ b/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart @@ -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, ); } } diff --git a/mobile/lib/modules/album/views/select_user_for_sharing_page.dart b/mobile/lib/modules/album/views/select_user_for_sharing_page.dart index eaf991645..01ffd3af9 100644 --- a/mobile/lib/modules/album/views/select_user_for_sharing_page.dart +++ b/mobile/lib/modules/album/views/select_user_for_sharing_page.dart @@ -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, ); } } diff --git a/mobile/lib/modules/home/ui/home_page_app_bar.dart b/mobile/lib/modules/home/ui/home_page_app_bar.dart index cff37b0bb..7a4f7fd56 100644 --- a/mobile/lib/modules/home/ui/home_page_app_bar.dart +++ b/mobile/lib/modules/home/ui/home_page_app_bar.dart @@ -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, ), ); } diff --git a/mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart b/mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart index da8a2c654..e0bf702f5 100644 --- a/mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart +++ b/mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart @@ -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) { diff --git a/mobile/lib/modules/home/ui/user_circle_avatar.dart b/mobile/lib/modules/home/ui/user_circle_avatar.dart deleted file mode 100644 index 441aab5cd..000000000 --- a/mobile/lib/modules/home/ui/user_circle_avatar.dart +++ /dev/null @@ -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), - ), - ), - ); - } -} diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 06f462ae5..c7a2333eb 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -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 { diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 7e92aebed..b1f7ec26d 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -296,6 +296,16 @@ class _$AppRouter extends RootStackRouter { ), ); }, + AlbumOptionsRoute.name: (routeData) { + final args = routeData.argsAs(); + return MaterialPageX( + routeData: routeData, + child: AlbumOptionsPage( + key: args.key, + album: args.album, + ), + ); + }, HomeRoute.name: (routeData) { return MaterialPageX( 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 { + 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 { diff --git a/mobile/lib/shared/ui/user_circle_avatar.dart b/mobile/lib/shared/ui/user_circle_avatar.dart new file mode 100644 index 000000000..c099ecbdf --- /dev/null +++ b/mobile/lib/shared/ui/user_circle_avatar.dart @@ -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), + ), + ), + ); + } +}