瀏覽代碼

refactor(mobile): app bar (#4687)

* refactor(mobile): add app bar to library and sharing

* mobile: add app bar dialog

* fix(mobile): refetch profile image only when path is changed

* mobile: add server url to dialog

* mobile: move trash to library app bar

* replace discord link with github

* user confirmation before sign out

* edit some styles

---------

Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
shenlong 1 年之前
父節點
當前提交
9f56bf0ab9

+ 7 - 1
mobile/assets/i18n/en-US.json

@@ -253,6 +253,8 @@
   "profile_drawer_settings": "Settings",
   "profile_drawer_sign_out": "Sign Out",
   "profile_drawer_trash": "Trash",
+  "profile_drawer_documentation": "Documentation",
+  "profile_drawer_github": "GitHub",
   "recently_added_page_title": "Recently Added",
   "search_bar_hint": "Search your photos",
   "search_page_categories": "Categories",
@@ -277,6 +279,7 @@
   "select_user_for_sharing_page_share_suggestions": "Suggestions",
   "server_info_box_app_version": "App Version",
   "server_info_box_server_version": "Server Version",
+  "server_info_box_server_url": "Server URL",
   "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).",
   "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).",
   "setting_image_viewer_original_title": "Load original image",
@@ -366,5 +369,8 @@
   "viewer_unstack": "Un-Stack",
   "cache_settings_tile_title": "Local Storage",
   "cache_settings_tile_subtitle": "Control the local storage behaviour",
-  "viewer_stack_use_as_main_asset": "Use as Main Asset"
+  "viewer_stack_use_as_main_asset": "Use as Main Asset",
+  "app_bar_signout_dialog_title": "Sign out",
+  "app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
+  "app_bar_signout_dialog_ok": "Yes"
 }

+ 20 - 16
mobile/lib/modules/album/views/library_page.dart

@@ -10,12 +10,16 @@ import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/models/album.dart';
 import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
+import 'package:immich_mobile/shared/providers/server_info.provider.dart';
+import 'package:immich_mobile/shared/ui/immich_app_bar.dart';
 
 class LibraryPage extends HookConsumerWidget {
   const LibraryPage({Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
+    final trashEnabled =
+        ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
     final albums = ref.watch(albumProvider);
     var isDarkMode = Theme.of(context).brightness == Brightness.dark;
     var settings = ref.watch(appSettingsServiceProvider);
@@ -28,21 +32,6 @@ class LibraryPage extends HookConsumerWidget {
       [],
     );
 
-    AppBar buildAppBar() {
-      return AppBar(
-        centerTitle: true,
-        automaticallyImplyLeading: false,
-        title: const Text(
-          'IMMICH',
-          style: TextStyle(
-            fontFamily: 'SnowburstOne',
-            fontWeight: FontWeight.bold,
-            fontSize: 22,
-          ),
-        ),
-      );
-    }
-
     final selectedAlbumSortOrder =
         useState(settings.getSetting(AppSettingsEnum.selectedAlbumSortOrder));
 
@@ -236,8 +225,23 @@ class LibraryPage extends HookConsumerWidget {
 
     final local = albums.where((a) => a.isLocal).toList();
 
+    Widget? shareTrashButton() {
+      return trashEnabled
+          ? InkWell(
+              onTap: () => AutoRouter.of(context).push(const TrashRoute()),
+              borderRadius: BorderRadius.circular(12),
+              child: const Icon(
+                Icons.delete_rounded,
+                size: 25,
+              ),
+            )
+          : null;
+    }
+
     return Scaffold(
-      appBar: buildAppBar(),
+      appBar: ImmichAppBar(
+        action: shareTrashButton(),
+      ),
       body: CustomScrollView(
         slivers: [
           SliverToBoxAdapter(

+ 15 - 27
mobile/lib/modules/album/views/sharing_page.dart

@@ -10,6 +10,7 @@ import 'package:immich_mobile/modules/partner/ui/partner_list.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/models/album.dart';
 import 'package:immich_mobile/shared/providers/user.provider.dart';
+import 'package:immich_mobile/shared/ui/immich_app_bar.dart';
 import 'package:immich_mobile/shared/ui/immich_image.dart';
 
 class SharingPage extends HookConsumerWidget {
@@ -167,32 +168,6 @@ class SharingPage extends HookConsumerWidget {
       );
     }
 
-    AppBar buildAppBar() {
-      return AppBar(
-        centerTitle: true,
-        automaticallyImplyLeading: false,
-        title: const Text(
-          'IMMICH',
-          style: TextStyle(
-            fontFamily: 'SnowburstOne',
-            fontWeight: FontWeight.bold,
-            fontSize: 22,
-          ),
-        ),
-        actions: [
-          IconButton(
-            splashRadius: 25,
-            iconSize: 20,
-            icon: const Icon(
-              Icons.swap_horizontal_circle_outlined,
-              size: 20,
-            ),
-            onPressed: () => AutoRouter.of(context).push(const PartnerRoute()),
-          ),
-        ],
-      );
-    }
-
     buildEmptyListIndication() {
       return SliverToBoxAdapter(
         child: Padding(
@@ -241,8 +216,21 @@ class SharingPage extends HookConsumerWidget {
       );
     }
 
+    Widget sharePartnerButton() {
+      return InkWell(
+        onTap: () => AutoRouter.of(context).push(const PartnerRoute()),
+        borderRadius: BorderRadius.circular(12),
+        child: const Icon(
+          Icons.swap_horizontal_circle_rounded,
+          size: 25,
+        ),
+      );
+    }
+
     return Scaffold(
-      appBar: buildAppBar(),
+      appBar: ImmichAppBar(
+        action: sharePartnerButton(),
+      ),
       body: CustomScrollView(
         slivers: [
           SliverToBoxAdapter(child: buildTopBottons()),

+ 0 - 41
mobile/lib/modules/backup/views/backup_controller_page.dart

@@ -174,46 +174,6 @@ class BackupControllerPage extends HookConsumerWidget {
       );
     }
 
-    Widget buildStorageInformation() {
-      return ListTile(
-        leading: Icon(
-          Icons.storage_rounded,
-          color: Theme.of(context).primaryColor,
-        ),
-        title: const Text(
-          "backup_controller_page_server_storage",
-          style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
-        ).tr(),
-        isThreeLine: true,
-        subtitle: Padding(
-          padding: const EdgeInsets.only(top: 8.0),
-          child: Column(
-            crossAxisAlignment: CrossAxisAlignment.start,
-            children: [
-              Padding(
-                padding: const EdgeInsets.only(top: 8.0),
-                child: LinearProgressIndicator(
-                  minHeight: 10.0,
-                  value: backupState.serverInfo.diskUsagePercentage / 100.0,
-                  backgroundColor: Colors.grey,
-                  color: Theme.of(context).primaryColor,
-                ),
-              ),
-              Padding(
-                padding: const EdgeInsets.only(top: 12.0),
-                child: const Text('backup_controller_page_storage_format').tr(
-                  args: [
-                    backupState.serverInfo.diskUse,
-                    backupState.serverInfo.diskSize,
-                  ],
-                ),
-              ),
-            ],
-          ),
-        ),
-      );
-    }
-
     ListTile buildAutoBackupController() {
       final isAutoBackup = backupState.autoBackup;
       final backUpOption = isAutoBackup
@@ -774,7 +734,6 @@ class BackupControllerPage extends HookConsumerWidget {
             if (showBackupFix) const Divider(),
             if (showBackupFix) buildCheckCorruptBackups(),
             const Divider(),
-            buildStorageInformation(),
             const Divider(),
             const CurrentUploadingAssetInfoBox(),
             if (!hasExclusiveAccess) buildBackgroundBackupInfo(),

+ 0 - 171
mobile/lib/modules/home/ui/home_page_app_bar.dart

@@ -1,171 +0,0 @@
-import 'package:auto_route/auto_route.dart';
-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/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/routing/router.dart';
-import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
-import 'package:immich_mobile/shared/models/server_info/server_info.model.dart';
-import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
-import 'package:immich_mobile/shared/providers/server_info.provider.dart';
-
-class HomePageAppBar extends ConsumerWidget implements PreferredSizeWidget {
-  @override
-  Size get preferredSize => const Size.fromHeight(kToolbarHeight);
-
-  const HomePageAppBar({
-    super.key,
-    this.onPopBack,
-  });
-
-  final Function? onPopBack;
-
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    final BackUpState backupState = ref.watch(backupProvider);
-    final bool isEnableAutoBackup =
-        backupState.backgroundBackup || backupState.autoBackup;
-    final ServerInfo serverInfoState = ref.watch(serverInfoProvider);
-    AuthenticationState authState = ref.watch(authenticationProvider);
-    final user = Store.tryGet(StoreKey.currentUser);
-    buildProfilePhoto() {
-      if (authState.profileImagePath.isEmpty || user == null) {
-        return IconButton(
-          splashRadius: 25,
-          icon: const Icon(
-            Icons.face_outlined,
-            size: 30,
-          ),
-          onPressed: () {
-            Scaffold.of(context).openDrawer();
-          },
-        );
-      } else {
-        return InkWell(
-          onTap: () {
-            Scaffold.of(context).openDrawer();
-          },
-          child: UserCircleAvatar(
-            radius: 18,
-            size: 33,
-            user: user,
-          ),
-        );
-      }
-    }
-
-    return AppBar(
-      backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
-      shape: const RoundedRectangleBorder(
-        borderRadius: BorderRadius.all(
-          Radius.circular(5),
-        ),
-      ),
-      leading: Builder(
-        builder: (BuildContext context) {
-          return Stack(
-            children: [
-              Center(
-                child: buildProfilePhoto(),
-              ),
-              if (serverInfoState.isVersionMismatch)
-                Positioned(
-                  bottom: 4,
-                  right: 6,
-                  child: GestureDetector(
-                    onTap: () => Scaffold.of(context).openDrawer(),
-                    child: Material(
-                      // color: Colors.grey[200],
-                      elevation: 1,
-                      shape: RoundedRectangleBorder(
-                        borderRadius: BorderRadius.circular(50.0),
-                      ),
-                      child: const Padding(
-                        padding: EdgeInsets.all(2.0),
-                        child: Icon(
-                          Icons.info,
-                          color: Color.fromARGB(255, 243, 188, 106),
-                          size: 15,
-                        ),
-                      ),
-                    ),
-                  ),
-                ),
-            ],
-          );
-        },
-      ),
-      title: const Text(
-        'IMMICH',
-        style: TextStyle(
-          fontFamily: 'SnowburstOne',
-          fontWeight: FontWeight.bold,
-          fontSize: 22,
-        ),
-      ),
-      actions: [
-        Stack(
-          alignment: AlignmentDirectional.center,
-          children: [
-            if (backupState.backupProgress == BackUpProgressEnum.inProgress)
-              Positioned(
-                top: 10,
-                right: 12,
-                child: SizedBox(
-                  height: 8,
-                  width: 8,
-                  child: CircularProgressIndicator(
-                    strokeWidth: 1,
-                    valueColor: AlwaysStoppedAnimation<Color>(
-                      Theme.of(context).primaryColor,
-                    ),
-                  ),
-                ),
-              ),
-            IconButton(
-              splashRadius: 25,
-              iconSize: 30,
-              icon: isEnableAutoBackup
-                  ? const Icon(
-                      Icons.backup_rounded,
-                    )
-                  : Badge(
-                      padding: const EdgeInsets.all(4),
-                      backgroundColor: Colors.white,
-                      label: const Icon(
-                        Icons.cloud_off_rounded,
-                        size: 8,
-                        color: Colors.indigo,
-                      ),
-                      child: Icon(
-                        Icons.backup_rounded,
-                        color: Theme.of(context).primaryColor,
-                      ),
-                    ),
-              onPressed: () async {
-                var onPop = await AutoRouter.of(context)
-                    .push(const BackupControllerRoute());
-
-                if (onPop != null && onPop == true) {
-                  onPopBack!();
-                }
-              },
-            ),
-            if (backupState.backupProgress == BackUpProgressEnum.inProgress)
-              Positioned(
-                bottom: 5,
-                child: Text(
-                  '${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}',
-                  style:
-                      const TextStyle(fontSize: 9, fontWeight: FontWeight.bold),
-                ),
-              ),
-          ],
-        ),
-      ],
-    );
-  }
-}

+ 0 - 144
mobile/lib/modules/home/ui/profile_drawer/profile_drawer.dart

@@ -1,144 +0,0 @@
-import 'package:auto_route/auto_route.dart';
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter/material.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
-import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
-import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer_header.dart';
-import 'package:immich_mobile/modules/home/ui/profile_drawer/server_info_box.dart';
-import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
-import 'package:immich_mobile/routing/router.dart';
-import 'package:immich_mobile/shared/providers/asset.provider.dart';
-import 'package:immich_mobile/shared/providers/server_info.provider.dart';
-import 'package:immich_mobile/shared/providers/websocket.provider.dart';
-
-class ProfileDrawer extends HookConsumerWidget {
-  const ProfileDrawer({Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    final trashEnabled =
-        ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
-
-    buildSignOutButton() {
-      return ListTile(
-        leading: SizedBox(
-          height: double.infinity,
-          child: Icon(
-            Icons.logout_rounded,
-            color: Theme.of(context).textTheme.labelMedium?.color,
-            size: 20,
-          ),
-        ),
-        title: Text(
-          "profile_drawer_sign_out",
-          style: Theme.of(context)
-              .textTheme
-              .labelLarge
-              ?.copyWith(fontWeight: FontWeight.bold),
-        ).tr(),
-        onTap: () async {
-          await ref.watch(authenticationProvider.notifier).logout();
-
-          ref.read(manualUploadProvider.notifier).cancelBackup();
-          ref.watch(backupProvider.notifier).cancelBackup();
-          ref.watch(assetProvider.notifier).clearAllAsset();
-          ref.watch(websocketProvider.notifier).disconnect();
-          AutoRouter.of(context).replace(const LoginRoute());
-        },
-      );
-    }
-
-    buildSettingButton() {
-      return ListTile(
-        leading: SizedBox(
-          height: double.infinity,
-          child: Icon(
-            Icons.settings_rounded,
-            color: Theme.of(context).textTheme.labelMedium?.color,
-            size: 20,
-          ),
-        ),
-        title: Text(
-          "profile_drawer_settings",
-          style: Theme.of(context)
-              .textTheme
-              .labelLarge
-              ?.copyWith(fontWeight: FontWeight.bold),
-        ).tr(),
-        onTap: () {
-          AutoRouter.of(context).push(const SettingsRoute());
-        },
-      );
-    }
-
-    buildAppLogButton() {
-      return ListTile(
-        leading: SizedBox(
-          height: double.infinity,
-          child: Icon(
-            Icons.assignment_outlined,
-            color: Theme.of(context).textTheme.labelMedium?.color,
-            size: 20,
-          ),
-        ),
-        title: Text(
-          "profile_drawer_app_logs",
-          style: Theme.of(context)
-              .textTheme
-              .labelLarge
-              ?.copyWith(fontWeight: FontWeight.bold),
-        ).tr(),
-        onTap: () {
-          AutoRouter.of(context).push(const AppLogRoute());
-        },
-      );
-    }
-
-    buildTrashButton() {
-      return ListTile(
-        leading: SizedBox(
-          height: double.infinity,
-          child: Icon(
-            Icons.delete_rounded,
-            color: Theme.of(context).textTheme.labelMedium?.color,
-            size: 20,
-          ),
-        ),
-        title: Text(
-          "profile_drawer_trash",
-          style: Theme.of(context)
-              .textTheme
-              .labelLarge
-              ?.copyWith(fontWeight: FontWeight.bold),
-        ).tr(),
-        onTap: () {
-          AutoRouter.of(context).push(const TrashRoute());
-        },
-      );
-    }
-
-    return Drawer(
-      shape: const RoundedRectangleBorder(
-        borderRadius: BorderRadius.zero,
-      ),
-      child: Column(
-        mainAxisAlignment: MainAxisAlignment.spaceBetween,
-        children: [
-          ListView(
-            shrinkWrap: true,
-            padding: EdgeInsets.zero,
-            children: [
-              const ProfileDrawerHeader(),
-              buildSettingButton(),
-              buildAppLogButton(),
-              if (trashEnabled) buildTrashButton(),
-              buildSignOutButton(),
-            ],
-          ),
-          const ServerInfoBox(),
-        ],
-      ),
-    );
-  }
-}

+ 0 - 126
mobile/lib/modules/home/ui/profile_drawer/server_info_box.dart

@@ -1,126 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/shared/models/server_info/server_info.model.dart';
-import 'package:easy_localization/easy_localization.dart';
-import 'package:immich_mobile/shared/providers/server_info.provider.dart';
-import 'package:package_info_plus/package_info_plus.dart';
-
-class ServerInfoBox extends HookConsumerWidget {
-  const ServerInfoBox({
-    Key? key,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    ServerInfo serverInfoState = ref.watch(serverInfoProvider);
-
-    final appInfo = useState({});
-
-    getPackageInfo() async {
-      PackageInfo packageInfo = await PackageInfo.fromPlatform();
-
-      appInfo.value = {
-        "version": packageInfo.version,
-        "buildNumber": packageInfo.buildNumber,
-      };
-    }
-
-    useEffect(
-      () {
-        getPackageInfo();
-        return null;
-      },
-      [],
-    );
-
-    return Padding(
-      padding: const EdgeInsets.all(8.0),
-      child: Card(
-        elevation: 0,
-        color: Theme.of(context).scaffoldBackgroundColor,
-        shape: RoundedRectangleBorder(
-          borderRadius: BorderRadius.circular(5), // if you need this
-          side: const BorderSide(
-            color: Color.fromARGB(101, 201, 201, 201),
-            width: 1,
-          ),
-        ),
-        child: Padding(
-          padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
-          child: Column(
-            crossAxisAlignment: CrossAxisAlignment.center,
-            children: [
-              Padding(
-                padding: const EdgeInsets.all(8.0),
-                child: Text(
-                  serverInfoState.isVersionMismatch
-                      ? serverInfoState.versionMismatchErrorMessage
-                      : "profile_drawer_client_server_up_to_date".tr(),
-                  textAlign: TextAlign.center,
-                  style: TextStyle(
-                    fontSize: 11,
-                    color: Theme.of(context).primaryColor,
-                    fontWeight: FontWeight.w600,
-                  ),
-                ),
-              ),
-              const Divider(
-                color: Color.fromARGB(101, 201, 201, 201),
-                thickness: 1,
-              ),
-              Row(
-                mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                children: [
-                  Text(
-                    "server_info_box_app_version".tr(),
-                    style: TextStyle(
-                      fontSize: 11,
-                      color: Colors.grey[500],
-                      fontWeight: FontWeight.bold,
-                    ),
-                  ),
-                  Text(
-                    "${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}",
-                    style: TextStyle(
-                      fontSize: 11,
-                      color: Colors.grey[500],
-                      fontWeight: FontWeight.bold,
-                    ),
-                  ),
-                ],
-              ),
-              const Divider(
-                color: Color.fromARGB(101, 201, 201, 201),
-                thickness: 1,
-              ),
-              Row(
-                mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                children: [
-                  Text(
-                    "server_info_box_server_version".tr(),
-                    style: TextStyle(
-                      fontSize: 11,
-                      color: Colors.grey[500],
-                      fontWeight: FontWeight.bold,
-                    ),
-                  ),
-                  Text(
-                    serverInfoState.serverVersion.major > 0
-                        ? "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}"
-                        : "?",
-                    style: TextStyle(
-                      fontSize: 11,
-                      color: Colors.grey[500],
-                      fontWeight: FontWeight.bold,
-                    ),
-                  ),
-                ],
-              ),
-            ],
-          ),
-        ),
-      ),
-    );
-  }
-}

+ 2 - 10
mobile/lib/modules/home/views/home_page.dart

@@ -17,9 +17,7 @@ import 'package:immich_mobile/modules/home/models/selection_state.dart';
 import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
-import 'package:immich_mobile/modules/home/ui/home_page_app_bar.dart';
 import 'package:immich_mobile/modules/memories/ui/memory_lane.dart';
-import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/models/album.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
@@ -27,6 +25,7 @@ import 'package:immich_mobile/shared/providers/asset.provider.dart';
 import 'package:immich_mobile/shared/providers/server_info.provider.dart';
 import 'package:immich_mobile/shared/providers/user.provider.dart';
 import 'package:immich_mobile/shared/providers/websocket.provider.dart';
+import 'package:immich_mobile/shared/ui/immich_app_bar.dart';
 import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/immich_toast.dart';
 import 'package:immich_mobile/utils/selection_handlers.dart';
@@ -74,10 +73,6 @@ class HomePage extends HookConsumerWidget {
       [],
     );
 
-    void reloadAllAsset() {
-      ref.watch(assetProvider.notifier).getAllAsset();
-    }
-
     Widget buildBody() {
       void selectionListener(
         bool multiselect,
@@ -375,10 +370,7 @@ class HomePage extends HookConsumerWidget {
     }
 
     return Scaffold(
-      appBar: !selectionEnabledHook.value
-          ? HomePageAppBar(onPopBack: reloadAllAsset)
-          : null,
-      drawer: const ProfileDrawer(),
+      appBar: !selectionEnabledHook.value ? const ImmichAppBar() : null,
       body: buildBody(),
     );
   }

+ 2 - 1
mobile/lib/modules/memories/ui/memory_lane.dart

@@ -16,7 +16,8 @@ class MemoryLane extends HookConsumerWidget {
     final memoryLane = memoryLaneFutureProvider
         .whenData(
           (memories) => memories != null
-              ? SizedBox(
+              ? Container(
+                  margin: const EdgeInsets.only(top: 10),
                   height: 200,
                   child: ListView.builder(
                     scrollDirection: Axis.horizontal,

+ 1 - 4
mobile/lib/routing/router.dart

@@ -133,10 +133,7 @@ part 'router.gr.dart';
         DuplicateGuard,
       ],
     ),
-    CustomRoute(
-      page: AppLogPage,
-      transitionsBuilder: TransitionsBuilders.slideBottom,
-    ),
+    AutoRoute(page: AppLogPage, guards: [DuplicateGuard]),
     AutoRoute(
       page: AppLogDetailPage,
     ),

+ 2 - 4
mobile/lib/routing/router.gr.dart

@@ -231,12 +231,9 @@ class _$AppRouter extends RootStackRouter {
       );
     },
     AppLogRoute.name: (routeData) {
-      return CustomPage<dynamic>(
+      return MaterialPageX<dynamic>(
         routeData: routeData,
         child: const AppLogPage(),
-        transitionsBuilder: TransitionsBuilders.slideBottom,
-        opaque: true,
-        barrierDismissible: false,
       );
     },
     AppLogDetailRoute.name: (routeData) {
@@ -583,6 +580,7 @@ class _$AppRouter extends RootStackRouter {
         RouteConfig(
           AppLogRoute.name,
           path: '/app-log-page',
+          guards: [duplicateGuard],
         ),
         RouteConfig(
           AppLogDetailRoute.name,

+ 263 - 0
mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart

@@ -0,0 +1,263 @@
+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:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
+import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
+import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
+import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
+import 'package:immich_mobile/routing/router.dart';
+import 'package:immich_mobile/shared/providers/asset.provider.dart';
+import 'package:immich_mobile/shared/providers/user.provider.dart';
+import 'package:immich_mobile/shared/providers/websocket.provider.dart';
+import 'package:immich_mobile/shared/ui/app_bar_dialog/app_bar_profile_info.dart';
+import 'package:immich_mobile/shared/ui/app_bar_dialog/app_bar_server_info.dart';
+import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
+import 'package:url_launcher/url_launcher.dart';
+
+class ImmichAppBarDialog extends HookConsumerWidget {
+  const ImmichAppBarDialog({super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    BackUpState backupState = ref.watch(backupProvider);
+    final theme = Theme.of(context);
+    bool isDarkTheme = theme.brightness == Brightness.dark;
+    bool isHorizontal = MediaQuery.of(context).size.width > 600;
+    final horizontalPadding = isHorizontal ? 100.0 : 20.0;
+    final user = ref.watch(currentUserProvider);
+
+    useEffect(
+      () {
+        ref.read(backupProvider.notifier).updateServerInfo();
+        return null;
+      },
+      [user],
+    );
+
+    buildTopRow() {
+      return Row(
+        children: [
+          InkWell(
+            onTap: () => Navigator.of(context).pop(),
+            child: const Icon(
+              Icons.close,
+              size: 20,
+            ),
+          ),
+          Expanded(
+            child: Align(
+              alignment: Alignment.center,
+              child: Text(
+                'IMMICH',
+                style: TextStyle(
+                  fontFamily: 'SnowburstOne',
+                  fontWeight: FontWeight.bold,
+                  color: Theme.of(context).primaryColor,
+                  fontSize: 15,
+                ),
+              ),
+            ),
+          ),
+        ],
+      );
+    }
+
+    buildActionButton(IconData icon, String text, Function() onTap) {
+      return ListTile(
+        dense: true,
+        visualDensity: VisualDensity.standard,
+        contentPadding: const EdgeInsets.only(left: 30),
+        minLeadingWidth: 40,
+        leading: SizedBox(
+          child: Icon(
+            icon,
+            color: theme.textTheme.labelMedium?.color,
+            size: 20,
+          ),
+        ),
+        title: Text(
+          text,
+          style:
+              theme.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
+        ).tr(),
+        onTap: onTap,
+      );
+    }
+
+    buildSettingButton() {
+      return buildActionButton(
+        Icons.settings_rounded,
+        "profile_drawer_settings",
+        () => AutoRouter.of(context).push(const SettingsRoute()),
+      );
+    }
+
+    buildAppLogButton() {
+      return buildActionButton(
+        Icons.assignment_outlined,
+        "profile_drawer_app_logs",
+        () => AutoRouter.of(context).push(const AppLogRoute()),
+      );
+    }
+
+    buildSignOutButton() {
+      return buildActionButton(
+        Icons.logout_rounded,
+        "profile_drawer_sign_out",
+        () async {
+          showDialog(
+            context: context,
+            builder: (BuildContext ctx) {
+              return ConfirmDialog(
+                title: "app_bar_signout_dialog_title",
+                content: "app_bar_signout_dialog_content",
+                ok: "app_bar_signout_dialog_ok",
+                onOk: () async {
+                  await ref.watch(authenticationProvider.notifier).logout();
+
+                  ref.read(manualUploadProvider.notifier).cancelBackup();
+                  ref.watch(backupProvider.notifier).cancelBackup();
+                  ref.watch(assetProvider.notifier).clearAllAsset();
+                  ref.watch(websocketProvider.notifier).disconnect();
+                  AutoRouter.of(context).replace(const LoginRoute());
+                },
+              );
+            },
+          );
+        },
+      );
+    }
+
+    Widget buildStorageInformation() {
+      return Padding(
+        padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 3),
+        child: Container(
+          padding: const EdgeInsets.symmetric(vertical: 4),
+          decoration: BoxDecoration(
+            color: isDarkTheme
+                ? Theme.of(context).scaffoldBackgroundColor
+                : const Color.fromARGB(255, 225, 229, 240),
+          ),
+          child: ListTile(
+            minLeadingWidth: 50,
+            leading: Icon(
+              Icons.storage_rounded,
+              color: theme.primaryColor,
+            ),
+            title: const Text(
+              "backup_controller_page_server_storage",
+              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
+            ).tr(),
+            isThreeLine: true,
+            subtitle: Padding(
+              padding: const EdgeInsets.only(top: 8.0),
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Padding(
+                    padding: const EdgeInsets.only(top: 8.0),
+                    child: LinearProgressIndicator(
+                      minHeight: 5.0,
+                      value: backupState.serverInfo.diskUsagePercentage / 100.0,
+                      backgroundColor: Colors.grey,
+                      color: theme.primaryColor,
+                    ),
+                  ),
+                  Padding(
+                    padding: const EdgeInsets.only(top: 12.0),
+                    child:
+                        const Text('backup_controller_page_storage_format').tr(
+                      args: [
+                        backupState.serverInfo.diskUse,
+                        backupState.serverInfo.diskSize,
+                      ],
+                    ),
+                  ),
+                ],
+              ),
+            ),
+          ),
+        ),
+      );
+    }
+
+    buildFooter() {
+      return Padding(
+        padding: const EdgeInsets.only(top: 10, bottom: 20),
+        child: Row(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            InkWell(
+              onTap: () {
+                Navigator.of(context).pop();
+                launchUrl(
+                  Uri.parse('https://immich.app'),
+                );
+              },
+              child: Text(
+                "profile_drawer_documentation",
+                style: Theme.of(context).textTheme.bodySmall,
+              ).tr(),
+            ),
+            const SizedBox(
+              width: 20,
+              child: Text(
+                "•",
+                textAlign: TextAlign.center,
+              ),
+            ),
+            InkWell(
+              onTap: () {
+                Navigator.of(context).pop();
+                launchUrl(
+                  Uri.parse('https://github.com/immich-app/immich'),
+                );
+              },
+              child: Text(
+                "profile_drawer_github",
+                style: Theme.of(context).textTheme.bodySmall,
+              ).tr(),
+            ),
+          ],
+        ),
+      );
+    }
+
+    return Dialog(
+      clipBehavior: Clip.hardEdge,
+      alignment: Alignment.topCenter,
+      insetPadding: EdgeInsets.only(
+        top: isHorizontal ? 20 : 60,
+        left: horizontalPadding,
+        right: horizontalPadding,
+        bottom: isHorizontal ? 20 : 100,
+      ),
+      backgroundColor: theme.cardColor,
+      shape: RoundedRectangleBorder(
+        borderRadius: BorderRadius.circular(20),
+      ),
+      child: SizedBox(
+        child: SingleChildScrollView(
+          child: Column(
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              Container(
+                padding: const EdgeInsets.all(20),
+                child: buildTopRow(),
+              ),
+              const AppBarProfileInfoBox(),
+              buildStorageInformation(),
+              const AppBarServerInfo(),
+              buildAppLogButton(),
+              buildSettingButton(),
+              buildSignOutButton(),
+              buildFooter(),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 53 - 67
mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart → mobile/lib/shared/ui/app_bar_dialog/app_bar_profile_info.dart

@@ -1,5 +1,4 @@
 import 'package:flutter/material.dart';
-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';
@@ -9,8 +8,8 @@ import 'package:immich_mobile/modules/login/models/authentication_state.model.da
 import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 
-class ProfileDrawerHeader extends HookConsumerWidget {
-  const ProfileDrawerHeader({
+class AppBarProfileInfoBox extends HookConsumerWidget {
+  const AppBarProfileInfoBox({
     Key? key,
   }) : super(key: key);
 
@@ -23,30 +22,24 @@ class ProfileDrawerHeader extends HookConsumerWidget {
     final user = Store.tryGet(StoreKey.currentUser);
 
     buildUserProfileImage() {
+      const immichImage = CircleAvatar(
+        radius: 20,
+        backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
+        backgroundColor: Colors.transparent,
+      );
+
       if (authState.profileImagePath.isEmpty || user == null) {
-        return const CircleAvatar(
-          radius: 35,
-          backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
-          backgroundColor: Colors.transparent,
-        );
+        return immichImage;
       }
 
-      var userImage = UserCircleAvatar(
-        radius: 35,
-        size: 66,
+      final userImage = UserCircleAvatar(
+        radius: 20,
+        size: 40,
         user: user,
       );
 
       if (uploadProfileImageStatus == UploadProfileStatus.idle) {
-        if (authState.profileImagePath.isNotEmpty) {
-          return userImage;
-        } else {
-          return const CircleAvatar(
-            radius: 33,
-            backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
-            backgroundColor: Colors.transparent,
-          );
-        }
+        return authState.profileImagePath.isNotEmpty ? userImage : immichImage;
       }
 
       if (uploadProfileImageStatus == UploadProfileStatus.success) {
@@ -54,18 +47,18 @@ class ProfileDrawerHeader extends HookConsumerWidget {
       }
 
       if (uploadProfileImageStatus == UploadProfileStatus.failure) {
-        return const CircleAvatar(
-          radius: 35,
-          backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
-          backgroundColor: Colors.transparent,
-        );
+        return immichImage;
       }
 
       if (uploadProfileImageStatus == UploadProfileStatus.loading) {
-        return const ImmichLoadingIndicator();
+        return const SizedBox(
+          height: 40,
+          width: 40,
+          child: ImmichLoadingIndicator(borderRadius: 20),
+        );
       }
 
-      return const SizedBox();
+      return immichImage;
     }
 
     pickUserProfileImage() async {
@@ -80,54 +73,45 @@ class ProfileDrawerHeader extends HookConsumerWidget {
             await ref.watch(uploadProfileImageProvider.notifier).upload(image);
 
         if (success) {
+          final profileImagePath =
+              ref.read(uploadProfileImageProvider).profileImagePath;
           ref.watch(authenticationProvider.notifier).updateUserProfileImagePath(
-                ref.read(uploadProfileImageProvider).profileImagePath,
+                profileImagePath,
               );
+          if (user != null) {
+            user.profileImagePath = profileImagePath;
+            Store.put(StoreKey.currentUser, user);
+          }
         }
       }
     }
 
-    useEffect(
-      () {
-        // buildUserProfileImage();
-        return null;
-      },
-      [],
-    );
-
-    return DrawerHeader(
-      decoration: BoxDecoration(
-        gradient: LinearGradient(
-          colors: isDarkMode
-              ? [
-                  const Color.fromARGB(255, 22, 25, 48),
-                  const Color.fromARGB(255, 13, 13, 13),
-                  const Color.fromARGB(255, 0, 0, 0),
-                ]
-              : [
-                  const Color.fromARGB(255, 216, 219, 238),
-                  const Color.fromARGB(255, 242, 242, 242),
-                  Colors.white,
-                ],
-          begin: Alignment.centerRight,
-          end: Alignment.centerLeft,
+    return Padding(
+      padding: const EdgeInsets.symmetric(horizontal: 10.0),
+      child: Container(
+        width: double.infinity,
+        decoration: BoxDecoration(
+          color: Theme.of(context).brightness == Brightness.dark
+              ? Theme.of(context).scaffoldBackgroundColor
+              : const Color.fromARGB(255, 225, 229, 240),
+          borderRadius: const BorderRadius.only(
+            topLeft: Radius.circular(10),
+            topRight: Radius.circular(10),
+          ),
         ),
-      ),
-      child: Column(
-        mainAxisAlignment: MainAxisAlignment.start,
-        crossAxisAlignment: CrossAxisAlignment.start,
-        children: [
-          GestureDetector(
+        child: ListTile(
+          minLeadingWidth: 50,
+          leading: GestureDetector(
             onTap: pickUserProfileImage,
             child: Stack(
               clipBehavior: Clip.none,
               children: [
                 buildUserProfileImage(),
                 Positioned(
-                  bottom: 0,
-                  right: -5,
+                  bottom: -5,
+                  right: -8,
                   child: Material(
-                    color: isDarkMode ? Colors.grey[700] : Colors.grey[100],
+                    color: isDarkMode ? Colors.blueGrey[800] : Colors.white,
                     elevation: 3,
                     shape: RoundedRectangleBorder(
                       borderRadius: BorderRadius.circular(50.0),
@@ -135,7 +119,7 @@ class ProfileDrawerHeader extends HookConsumerWidget {
                     child: Padding(
                       padding: const EdgeInsets.all(5.0),
                       child: Icon(
-                        Icons.edit,
+                        Icons.camera_alt_outlined,
                         color: Theme.of(context).primaryColor,
                         size: 14,
                       ),
@@ -145,19 +129,21 @@ class ProfileDrawerHeader extends HookConsumerWidget {
               ],
             ),
           ),
-          Text(
+          title: Text(
             "${authState.firstName} ${authState.lastName}",
             style: TextStyle(
               color: Theme.of(context).primaryColor,
               fontWeight: FontWeight.bold,
-              fontSize: 24,
+              fontSize: 16,
             ),
           ),
-          Text(
+          subtitle: Text(
             authState.userEmail,
-            style: Theme.of(context).textTheme.labelMedium,
+            style: Theme.of(context).textTheme.labelMedium?.copyWith(
+                  fontSize: 12,
+                ),
           ),
-        ],
+        ),
       ),
     );
   }

+ 209 - 0
mobile/lib/shared/ui/app_bar_dialog/app_bar_server_info.dart

@@ -0,0 +1,209 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/shared/models/server_info/server_info.model.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:immich_mobile/shared/providers/server_info.provider.dart';
+import 'package:immich_mobile/utils/url_helper.dart';
+import 'package:package_info_plus/package_info_plus.dart';
+
+class AppBarServerInfo extends HookConsumerWidget {
+  const AppBarServerInfo({
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    ServerInfo serverInfoState = ref.watch(serverInfoProvider);
+
+    final appInfo = useState({});
+
+    getPackageInfo() async {
+      PackageInfo packageInfo = await PackageInfo.fromPlatform();
+
+      appInfo.value = {
+        "version": packageInfo.version,
+        "buildNumber": packageInfo.buildNumber,
+      };
+    }
+
+    useEffect(
+      () {
+        getPackageInfo();
+        return null;
+      },
+      [],
+    );
+
+    return Padding(
+      padding: const EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10.0),
+      child: Container(
+        decoration: BoxDecoration(
+          color: Theme.of(context).brightness == Brightness.dark
+              ? Theme.of(context).scaffoldBackgroundColor
+              : const Color.fromARGB(255, 225, 229, 240),
+          borderRadius: const BorderRadius.only(
+            bottomLeft: Radius.circular(10),
+            bottomRight: Radius.circular(10),
+          ),
+        ),
+        child: Padding(
+          padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              Padding(
+                padding: const EdgeInsets.all(8.0),
+                child: Text(
+                  serverInfoState.isVersionMismatch
+                      ? serverInfoState.versionMismatchErrorMessage
+                      : "profile_drawer_client_server_up_to_date".tr(),
+                  textAlign: TextAlign.center,
+                  style: TextStyle(
+                    fontSize: 11,
+                    color: Theme.of(context).primaryColor,
+                    fontWeight: FontWeight.w600,
+                  ),
+                ),
+              ),
+              const Padding(
+                padding: EdgeInsets.symmetric(horizontal: 10),
+                child: Divider(
+                  color: Color.fromARGB(101, 201, 201, 201),
+                  thickness: 1,
+                ),
+              ),
+              Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                children: [
+                  Expanded(
+                    child: Padding(
+                      padding: const EdgeInsets.only(left: 10.0),
+                      child: Text(
+                        "server_info_box_app_version".tr(),
+                        style: TextStyle(
+                          fontSize: 11,
+                          color: Theme.of(context).textTheme.labelSmall?.color,
+                          fontWeight: FontWeight.bold,
+                        ),
+                      ),
+                    ),
+                  ),
+                  Expanded(
+                    flex: 0,
+                    child: Padding(
+                      padding: const EdgeInsets.only(right: 10.0),
+                      child: Text(
+                        "${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}",
+                        style: TextStyle(
+                          fontSize: 11,
+                          color: Theme.of(context)
+                              .textTheme
+                              .labelSmall
+                              ?.color
+                              ?.withOpacity(0.5),
+                          fontWeight: FontWeight.bold,
+                        ),
+                      ),
+                    ),
+                  ),
+                ],
+              ),
+              const Padding(
+                padding: EdgeInsets.symmetric(horizontal: 10),
+                child: Divider(
+                  color: Color.fromARGB(101, 201, 201, 201),
+                  thickness: 1,
+                ),
+              ),
+              Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                children: [
+                  Expanded(
+                    child: Padding(
+                      padding: const EdgeInsets.only(left: 10.0),
+                      child: Text(
+                        "server_info_box_server_version".tr(),
+                        style: TextStyle(
+                          fontSize: 11,
+                          color: Theme.of(context).textTheme.labelSmall?.color,
+                          fontWeight: FontWeight.bold,
+                        ),
+                      ),
+                    ),
+                  ),
+                  Expanded(
+                    flex: 0,
+                    child: Padding(
+                      padding: const EdgeInsets.only(right: 10.0),
+                      child: Text(
+                        serverInfoState.serverVersion.major > 0
+                            ? "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}"
+                            : "?",
+                        style: TextStyle(
+                          fontSize: 11,
+                          color: Theme.of(context)
+                              .textTheme
+                              .labelSmall
+                              ?.color
+                              ?.withOpacity(0.5),
+                          fontWeight: FontWeight.bold,
+                        ),
+                      ),
+                    ),
+                  ),
+                ],
+              ),
+              const Padding(
+                padding: EdgeInsets.symmetric(horizontal: 10),
+                child: Divider(
+                  color: Color.fromARGB(101, 201, 201, 201),
+                  thickness: 1,
+                ),
+              ),
+              Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                children: [
+                  Expanded(
+                    child: Padding(
+                      padding: const EdgeInsets.only(left: 10.0),
+                      child: Text(
+                        "server_info_box_server_url".tr(),
+                        style: TextStyle(
+                          fontSize: 11,
+                          color: Theme.of(context).textTheme.labelSmall?.color,
+                          fontWeight: FontWeight.bold,
+                        ),
+                      ),
+                    ),
+                  ),
+                  Expanded(
+                    flex: 0,
+                    child: Container(
+                      width: 200,
+                      padding: const EdgeInsets.only(right: 10.0),
+                      child: Text(
+                        getServerUrl() ?? '--',
+                        style: TextStyle(
+                          fontSize: 11,
+                          color: Theme.of(context)
+                              .textTheme
+                              .labelSmall
+                              ?.color
+                              ?.withOpacity(0.5),
+                          fontWeight: FontWeight.bold,
+                          overflow: TextOverflow.ellipsis,
+                        ),
+                        textAlign: TextAlign.end,
+                      ),
+                    ),
+                  ),
+                ],
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 192 - 0
mobile/lib/shared/ui/immich_app_bar.dart

@@ -0,0 +1,192 @@
+import 'package:auto_route/auto_route.dart';
+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/ui/app_bar_dialog/app_bar_dialog.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/routing/router.dart';
+import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
+import 'package:immich_mobile/shared/models/server_info/server_info.model.dart';
+import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
+import 'package:immich_mobile/shared/providers/server_info.provider.dart';
+
+class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
+  @override
+  Size get preferredSize => const Size.fromHeight(kToolbarHeight);
+  final Widget? action;
+
+  const ImmichAppBar({super.key, this.action});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final BackUpState backupState = ref.watch(backupProvider);
+    final bool isEnableAutoBackup =
+        backupState.backgroundBackup || backupState.autoBackup;
+    final ServerInfo serverInfoState = ref.watch(serverInfoProvider);
+    AuthenticationState authState = ref.watch(authenticationProvider);
+    final user = Store.tryGet(StoreKey.currentUser);
+    final isDarkMode = Theme.of(context).brightness == Brightness.dark;
+    const widgetSize = 30.0;
+
+    buildProfilePhoto() {
+      return InkWell(
+        onTap: () => showDialog(
+          context: context,
+          useRootNavigator: false,
+          builder: (ctx) => const ImmichAppBarDialog(),
+        ),
+        borderRadius: BorderRadius.circular(12),
+        child: authState.profileImagePath.isEmpty || user == null
+            ? const Icon(
+                Icons.face_outlined,
+                size: widgetSize,
+              )
+            : UserCircleAvatar(
+                radius: 15,
+                size: 27,
+                user: user,
+              ),
+      );
+    }
+
+    buildProfileIndicator() {
+      return Badge(
+        label: Container(
+          decoration: BoxDecoration(
+            color: Colors.black,
+            borderRadius: BorderRadius.circular(widgetSize / 2),
+          ),
+          child: const Icon(
+            Icons.info,
+            color: Color.fromARGB(255, 243, 188, 106),
+            size: widgetSize / 2,
+          ),
+        ),
+        backgroundColor: Colors.transparent,
+        alignment: Alignment.bottomRight,
+        isLabelVisible: serverInfoState.isVersionMismatch,
+        offset: const Offset(2, 2),
+        child: buildProfilePhoto(),
+      );
+    }
+
+    getBackupBadgeIcon() {
+      final iconColor = isDarkMode ? Colors.white : Colors.black;
+
+      if (isEnableAutoBackup) {
+        if (backupState.backupProgress == BackUpProgressEnum.inProgress) {
+          return Container(
+            padding: const EdgeInsets.all(3.5),
+            child: CircularProgressIndicator(
+              strokeWidth: 2,
+              strokeCap: StrokeCap.round,
+              valueColor: AlwaysStoppedAnimation<Color>(iconColor),
+            ),
+          );
+        } else if (backupState.backupProgress !=
+                BackUpProgressEnum.inBackground &&
+            backupState.backupProgress != BackUpProgressEnum.manualInProgress) {
+          return Icon(
+            Icons.check_outlined,
+            size: 9,
+            color: iconColor,
+          );
+        }
+      }
+
+      if (!isEnableAutoBackup) {
+        return Icon(
+          Icons.cloud_off_rounded,
+          size: 9,
+          color: iconColor,
+        );
+      }
+    }
+
+    buildBackupIndicator() {
+      final indicatorIcon = getBackupBadgeIcon();
+      final badgeBackground = isDarkMode ? Colors.blueGrey[800] : Colors.white;
+
+      return InkWell(
+        onTap: () => AutoRouter.of(context).push(const BackupControllerRoute()),
+        borderRadius: BorderRadius.circular(12),
+        child: Badge(
+          label: Container(
+            width: widgetSize / 2,
+            height: widgetSize / 2,
+            decoration: BoxDecoration(
+              color: badgeBackground,
+              border: Border.all(
+                color: isDarkMode ? Colors.black : Colors.grey,
+              ),
+              borderRadius: BorderRadius.circular(widgetSize / 2),
+            ),
+            child: indicatorIcon,
+          ),
+          backgroundColor: Colors.transparent,
+          alignment: Alignment.bottomRight,
+          isLabelVisible: indicatorIcon != null,
+          offset: const Offset(2, 2),
+          child: Icon(
+            Icons.backup_rounded,
+            size: widgetSize,
+            color: Theme.of(context).primaryColor,
+          ),
+        ),
+      );
+    }
+
+    return AppBar(
+      backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
+      shape: const RoundedRectangleBorder(
+        borderRadius: BorderRadius.all(
+          Radius.circular(5),
+        ),
+      ),
+      automaticallyImplyLeading: false,
+      centerTitle: false,
+      title: Builder(
+        builder: (BuildContext context) {
+          return Row(
+            children: [
+              Container(
+                padding: const EdgeInsets.only(top: 3),
+                width: 28,
+                height: 28,
+                child: Image.asset(
+                  'assets/immich-logo.png',
+                ),
+              ),
+              Container(
+                margin: const EdgeInsets.only(left: 10),
+                child: const Text(
+                  'IMMICH',
+                  style: TextStyle(
+                    fontFamily: 'SnowburstOne',
+                    fontWeight: FontWeight.bold,
+                    fontSize: 24,
+                  ),
+                ),
+              ),
+            ],
+          );
+        },
+      ),
+      actions: [
+        if (action != null)
+          Padding(padding: const EdgeInsets.only(right: 20), child: action!),
+        Padding(
+          padding: const EdgeInsets.only(right: 20),
+          child: buildBackupIndicator(),
+        ),
+        Padding(
+          padding: const EdgeInsets.only(right: 20),
+          child: buildProfileIndicator(),
+        ),
+      ],
+    );
+  }
+}

+ 4 - 1
mobile/lib/shared/ui/immich_loading_indicator.dart

@@ -1,8 +1,11 @@
 import 'package:flutter/material.dart';
 
 class ImmichLoadingIndicator extends StatelessWidget {
+  final double? borderRadius;
+
   const ImmichLoadingIndicator({
     Key? key,
+    this.borderRadius,
   }) : super(key: key);
 
   @override
@@ -12,7 +15,7 @@ class ImmichLoadingIndicator extends StatelessWidget {
       width: 60,
       decoration: BoxDecoration(
         color: Theme.of(context).primaryColor.withAlpha(200),
-        borderRadius: BorderRadius.circular(10),
+        borderRadius: BorderRadius.circular(borderRadius ?? 10),
       ),
       padding: const EdgeInsets.all(15),
       child: const CircularProgressIndicator(

+ 11 - 11
mobile/lib/shared/ui/user_circle_avatar.dart

@@ -1,5 +1,6 @@
 import 'dart:math';
 
+import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/shared/models/store.dart';
@@ -46,7 +47,7 @@ class UserCircleAvatar extends ConsumerWidget {
       radius: radius,
       child: user.profileImagePath == ""
           ? Text(
-              user.firstName[0],
+              user.firstName[0].toUpperCase(),
               style: const TextStyle(
                 fontWeight: FontWeight.bold,
                 color: Colors.black,
@@ -54,19 +55,18 @@ class UserCircleAvatar extends ConsumerWidget {
             )
           : ClipRRect(
               borderRadius: BorderRadius.circular(50),
-              child: FadeInImage(
+              child: CachedNetworkImage(
                 fit: BoxFit.cover,
-                placeholder: MemoryImage(kTransparentImage),
+                cacheKey: user.profileImagePath,
                 width: size,
                 height: size,
-                image: NetworkImage(
-                  profileImageUrl,
-                  headers: {
-                    "Authorization": "Bearer ${Store.get(StoreKey.accessToken)}",
-                  },
-                ),
-                fadeInDuration: const Duration(milliseconds: 200),
-                imageErrorBuilder: (context, error, stackTrace) =>
+                placeholder: (_, __) => Image.memory(kTransparentImage),
+                imageUrl: profileImageUrl,
+                httpHeaders: {
+                  "Authorization": "Bearer ${Store.get(StoreKey.accessToken)}",
+                },
+                fadeInDuration: const Duration(milliseconds: 300),
+                errorWidget: (context, error, stackTrace) =>
                     Image.memory(kTransparentImage),
               ),
             ),