From 9f56bf0ab9b851100b823ecca018e74ef5f9d5ce Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Mon, 30 Oct 2023 17:17:34 +0000 Subject: [PATCH] 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 --- mobile/assets/i18n/en-US.json | 8 +- .../lib/modules/album/views/library_page.dart | 36 +-- .../lib/modules/album/views/sharing_page.dart | 42 +-- .../backup/views/backup_controller_page.dart | 41 --- .../modules/home/ui/home_page_app_bar.dart | 171 ------------ .../ui/profile_drawer/profile_drawer.dart | 144 ---------- .../ui/profile_drawer/server_info_box.dart | 126 --------- mobile/lib/modules/home/views/home_page.dart | 12 +- .../lib/modules/memories/ui/memory_lane.dart | 3 +- mobile/lib/routing/router.dart | 5 +- mobile/lib/routing/router.gr.dart | 6 +- .../ui/app_bar_dialog/app_bar_dialog.dart | 263 ++++++++++++++++++ .../app_bar_dialog/app_bar_profile_info.dart} | 120 ++++---- .../app_bar_dialog/app_bar_server_info.dart | 209 ++++++++++++++ mobile/lib/shared/ui/immich_app_bar.dart | 192 +++++++++++++ .../shared/ui/immich_loading_indicator.dart | 5 +- mobile/lib/shared/ui/user_circle_avatar.dart | 22 +- 17 files changed, 781 insertions(+), 624 deletions(-) delete mode 100644 mobile/lib/modules/home/ui/home_page_app_bar.dart delete mode 100644 mobile/lib/modules/home/ui/profile_drawer/profile_drawer.dart delete mode 100644 mobile/lib/modules/home/ui/profile_drawer/server_info_box.dart create mode 100644 mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart rename mobile/lib/{modules/home/ui/profile_drawer/profile_drawer_header.dart => shared/ui/app_bar_dialog/app_bar_profile_info.dart} (57%) create mode 100644 mobile/lib/shared/ui/app_bar_dialog/app_bar_server_info.dart create mode 100644 mobile/lib/shared/ui/immich_app_bar.dart diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index be576aa5c2..4a77aac1c2 100644 --- a/mobile/assets/i18n/en-US.json +++ b/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" } diff --git a/mobile/lib/modules/album/views/library_page.dart b/mobile/lib/modules/album/views/library_page.dart index 0eb44b82c5..90d0956409 100644 --- a/mobile/lib/modules/album/views/library_page.dart +++ b/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( diff --git a/mobile/lib/modules/album/views/sharing_page.dart b/mobile/lib/modules/album/views/sharing_page.dart index 9d0593d286..5e74aef679 100644 --- a/mobile/lib/modules/album/views/sharing_page.dart +++ b/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()), diff --git a/mobile/lib/modules/backup/views/backup_controller_page.dart b/mobile/lib/modules/backup/views/backup_controller_page.dart index 0901a4d1d1..9dc40ecede 100644 --- a/mobile/lib/modules/backup/views/backup_controller_page.dart +++ b/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(), diff --git a/mobile/lib/modules/home/ui/home_page_app_bar.dart b/mobile/lib/modules/home/ui/home_page_app_bar.dart deleted file mode 100644 index cbe1846cef..0000000000 --- a/mobile/lib/modules/home/ui/home_page_app_bar.dart +++ /dev/null @@ -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( - 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), - ), - ), - ], - ), - ], - ); - } -} diff --git a/mobile/lib/modules/home/ui/profile_drawer/profile_drawer.dart b/mobile/lib/modules/home/ui/profile_drawer/profile_drawer.dart deleted file mode 100644 index affa7cb64a..0000000000 --- a/mobile/lib/modules/home/ui/profile_drawer/profile_drawer.dart +++ /dev/null @@ -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(), - ], - ), - ); - } -} diff --git a/mobile/lib/modules/home/ui/profile_drawer/server_info_box.dart b/mobile/lib/modules/home/ui/profile_drawer/server_info_box.dart deleted file mode 100644 index fe050b542e..0000000000 --- a/mobile/lib/modules/home/ui/profile_drawer/server_info_box.dart +++ /dev/null @@ -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, - ), - ), - ], - ), - ], - ), - ), - ), - ); - } -} diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index d84fc153c5..b1800e8e60 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/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(), ); } diff --git a/mobile/lib/modules/memories/ui/memory_lane.dart b/mobile/lib/modules/memories/ui/memory_lane.dart index 9021546072..dcd8036512 100644 --- a/mobile/lib/modules/memories/ui/memory_lane.dart +++ b/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, diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 0c3da5c022..a4cc2401f2 100644 --- a/mobile/lib/routing/router.dart +++ b/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, ), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 12110cf62a..b5b5b773af 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -231,12 +231,9 @@ class _$AppRouter extends RootStackRouter { ); }, AppLogRoute.name: (routeData) { - return CustomPage( + return MaterialPageX( 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, diff --git a/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart new file mode 100644 index 0000000000..b17fce86d3 --- /dev/null +++ b/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(), + ], + ), + ), + ), + ); + } +} diff --git a/mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart b/mobile/lib/shared/ui/app_bar_dialog/app_bar_profile_info.dart similarity index 57% rename from mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart rename to mobile/lib/shared/ui/app_bar_dialog/app_bar_profile_info.dart index 328d9cbc1f..d58699d5c0 100644 --- a/mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart +++ b/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, + ), ), - ], + ), ), ); } diff --git a/mobile/lib/shared/ui/app_bar_dialog/app_bar_server_info.dart b/mobile/lib/shared/ui/app_bar_dialog/app_bar_server_info.dart new file mode 100644 index 0000000000..8ef3c09b5a --- /dev/null +++ b/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, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/mobile/lib/shared/ui/immich_app_bar.dart b/mobile/lib/shared/ui/immich_app_bar.dart new file mode 100644 index 0000000000..ad8195354b --- /dev/null +++ b/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(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(), + ), + ], + ); + } +} diff --git a/mobile/lib/shared/ui/immich_loading_indicator.dart b/mobile/lib/shared/ui/immich_loading_indicator.dart index bd4ad0d3cb..98ddb8f47e 100644 --- a/mobile/lib/shared/ui/immich_loading_indicator.dart +++ b/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( diff --git a/mobile/lib/shared/ui/user_circle_avatar.dart b/mobile/lib/shared/ui/user_circle_avatar.dart index 39f27dbd9f..b70566d88d 100644 --- a/mobile/lib/shared/ui/user_circle_avatar.dart +++ b/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), ), ),