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