Quellcode durchsuchen

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>
Alex vor 1 Jahr
Ursprung
Commit
2de30e34f4

+ 3 - 2
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"
-}
+  "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
+  "translated_text_options": "Options"
+}

+ 10 - 0
mobile/lib/modules/album/providers/shared_album.provider.dart

@@ -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();

+ 20 - 0
mobile/lib/modules/album/services/album.service.dart

@@ -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,

+ 5 - 0
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)

+ 98 - 32
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<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,

+ 5 - 0
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,
+        ),
       ),
     );
   }

+ 205 - 0
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(),
+        ],
+      ),
+    );
+  }
+}

+ 32 - 31
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),
         ],
       );
     }

+ 5 - 2
mobile/lib/modules/album/views/asset_selection_page.dart

@@ -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(),
             ),
         ],

+ 4 - 2
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<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,
                 ),
               ),
             ),

+ 3 - 4
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,
         );
       }
     }

+ 3 - 4
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,
         );
       }
     }

+ 5 - 3
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,
           ),
         );
       }

+ 5 - 2
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) {

+ 0 - 44
mobile/lib/modules/home/ui/user_circle_avatar.dart

@@ -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 - 0
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 {

+ 52 - 0
mobile/lib/routing/router.gr.dart

@@ -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 - 0
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),
+              ),
+            ),
+    );
+  }
+}