mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	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>
This commit is contained in:
		
							parent
							
								
									603b056512
								
							
						
					
					
						commit
						9f56bf0ab9
					
				@ -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"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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(
 | 
			
		||||
 | 
			
		||||
@ -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()),
 | 
			
		||||
 | 
			
		||||
@ -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(),
 | 
			
		||||
 | 
			
		||||
@ -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),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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(),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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,
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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(),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -133,10 +133,7 @@ part 'router.gr.dart';
 | 
			
		||||
        DuplicateGuard,
 | 
			
		||||
      ],
 | 
			
		||||
    ),
 | 
			
		||||
    CustomRoute(
 | 
			
		||||
      page: AppLogPage,
 | 
			
		||||
      transitionsBuilder: TransitionsBuilders.slideBottom,
 | 
			
		||||
    ),
 | 
			
		||||
    AutoRoute(page: AppLogPage, guards: [DuplicateGuard]),
 | 
			
		||||
    AutoRoute(
 | 
			
		||||
      page: AppLogDetailPage,
 | 
			
		||||
    ),
 | 
			
		||||
 | 
			
		||||
@ -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
									
								
								mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										263
									
								
								mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart
									
									
									
									
									
										Normal file
									
								
							@ -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(),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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() {
 | 
			
		||||
      if (authState.profileImagePath.isEmpty || user == null) {
 | 
			
		||||
        return const CircleAvatar(
 | 
			
		||||
          radius: 35,
 | 
			
		||||
      const immichImage = CircleAvatar(
 | 
			
		||||
        radius: 20,
 | 
			
		||||
        backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
 | 
			
		||||
        backgroundColor: Colors.transparent,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (authState.profileImagePath.isEmpty || user == null) {
 | 
			
		||||
        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(
 | 
			
		||||
    return Padding(
 | 
			
		||||
      padding: const EdgeInsets.symmetric(horizontal: 10.0),
 | 
			
		||||
      child: Container(
 | 
			
		||||
        width: double.infinity,
 | 
			
		||||
        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,
 | 
			
		||||
          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
									
								
								mobile/lib/shared/ui/app_bar_dialog/app_bar_server_info.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								mobile/lib/shared/ui/app_bar_dialog/app_bar_server_info.dart
									
									
									
									
									
										Normal file
									
								
							@ -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
									
								
								mobile/lib/shared/ui/immich_app_bar.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								mobile/lib/shared/ui/immich_app_bar.dart
									
									
									
									
									
										Normal file
									
								
							@ -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(),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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(
 | 
			
		||||
 | 
			
		||||
@ -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: {
 | 
			
		||||
                placeholder: (_, __) => Image.memory(kTransparentImage),
 | 
			
		||||
                imageUrl: profileImageUrl,
 | 
			
		||||
                httpHeaders: {
 | 
			
		||||
                  "Authorization": "Bearer ${Store.get(StoreKey.accessToken)}",
 | 
			
		||||
                },
 | 
			
		||||
                ),
 | 
			
		||||
                fadeInDuration: const Duration(milliseconds: 200),
 | 
			
		||||
                imageErrorBuilder: (context, error, stackTrace) =>
 | 
			
		||||
                fadeInDuration: const Duration(milliseconds: 300),
 | 
			
		||||
                errorWidget: (context, error, stackTrace) =>
 | 
			
		||||
                    Image.memory(kTransparentImage),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user