mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	Add settings screen on mobile (#463)
* Refactor profile drawer to sub component * Added setting page, routing with some options * Added setting service * Implement three stage settings * get app setting for three stage loading
This commit is contained in:
		| @@ -75,7 +75,8 @@ | |||||||
|   "login_form_save_login": "Stay logged in", |   "login_form_save_login": "Stay logged in", | ||||||
|   "monthly_title_text_date_format": "MMMM y", |   "monthly_title_text_date_format": "MMMM y", | ||||||
|   "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", |   "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", | ||||||
|   "profile_drawer_sign_out": "Sign Out", |   "profile_drawer_sign_out": "Sign out", | ||||||
|  |   "profile_drawer_settings": "Settings", | ||||||
|   "search_bar_hint": "Search your photos", |   "search_bar_hint": "Search your photos", | ||||||
|   "search_page_no_objects": "No Objects Info Available", |   "search_page_no_objects": "No Objects Info Available", | ||||||
|   "search_page_no_places": "No Places Info Available", |   "search_page_no_places": "No Places Info Available", | ||||||
|   | |||||||
| @@ -16,3 +16,6 @@ const String backupInfoKey = "immichBackupAlbumInfoKey"; // Key 1 | |||||||
| // Github Release Info | // Github Release Info | ||||||
| const String hiveGithubReleaseInfoBox = "immichGithubReleaseInfoBox"; // Box | const String hiveGithubReleaseInfoBox = "immichGithubReleaseInfoBox"; // Box | ||||||
| const String githubReleaseInfoKey = "immichGithubReleaseInfoKey"; // Key 1 | const String githubReleaseInfoKey = "immichGithubReleaseInfoKey"; // Key 1 | ||||||
|  |  | ||||||
|  | // User Setting Info | ||||||
|  | const String userSettingInfoBox = "immichUserSettingInfoBox"; | ||||||
|   | |||||||
| @@ -33,6 +33,7 @@ void main() async { | |||||||
|   await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox); |   await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox); | ||||||
|   await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox); |   await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox); | ||||||
|   await Hive.openBox(hiveGithubReleaseInfoBox); |   await Hive.openBox(hiveGithubReleaseInfoBox); | ||||||
|  |   await Hive.openBox(userSettingInfoBox); | ||||||
|  |  | ||||||
|   SystemChrome.setSystemUIOverlayStyle( |   SystemChrome.setSystemUIOverlayStyle( | ||||||
|     const SystemUiOverlayStyle( |     const SystemUiOverlayStyle( | ||||||
|   | |||||||
| @@ -56,11 +56,11 @@ class _RemotePhotoViewState extends State<RemotePhotoView> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _fireStartLoadingEvent() { |   void _fireStartLoadingEvent() { | ||||||
|     if (widget.onLoadingStart != null) widget.onLoadingStart!(); |     widget.onLoadingStart(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _fireFinishedLoadingEvent() { |   void _fireFinishedLoadingEvent() { | ||||||
|     if (widget.onLoadingCompleted != null) widget.onLoadingCompleted!(); |     widget.onLoadingCompleted(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   CachedNetworkImageProvider _authorizedImageProvider(String url) { |   CachedNetworkImageProvider _authorizedImageProvider(String url) { | ||||||
| @@ -141,26 +141,26 @@ class _RemotePhotoViewState extends State<RemotePhotoView> { | |||||||
| } | } | ||||||
|  |  | ||||||
| class RemotePhotoView extends StatefulWidget { | class RemotePhotoView extends StatefulWidget { | ||||||
|   const RemotePhotoView( |   const RemotePhotoView({ | ||||||
|       {Key? key, |     Key? key, | ||||||
|       required this.thumbnailUrl, |     required this.thumbnailUrl, | ||||||
|       required this.imageUrl, |     required this.imageUrl, | ||||||
|       required this.authToken, |     required this.authToken, | ||||||
|       required this.isZoomedFunction, |     required this.isZoomedFunction, | ||||||
|       required this.isZoomedListener, |     required this.isZoomedListener, | ||||||
|       required this.onSwipeDown, |     required this.onSwipeDown, | ||||||
|       required this.onSwipeUp, |     required this.onSwipeUp, | ||||||
|       this.previewUrl, |     this.previewUrl, | ||||||
|       this.onLoadingCompleted, |     required this.onLoadingCompleted, | ||||||
|       this.onLoadingStart}) |     required this.onLoadingStart, | ||||||
|       : super(key: key); |   }) : super(key: key); | ||||||
|  |  | ||||||
|   final String thumbnailUrl; |   final String thumbnailUrl; | ||||||
|   final String imageUrl; |   final String imageUrl; | ||||||
|   final String authToken; |   final String authToken; | ||||||
|   final String? previewUrl; |   final String? previewUrl; | ||||||
|   final Function? onLoadingCompleted; |   final Function onLoadingCompleted; | ||||||
|   final Function? onLoadingStart; |   final Function onLoadingStart; | ||||||
|  |  | ||||||
|   final void Function() onSwipeDown; |   final void Function() onSwipeDown; | ||||||
|   final void Function() onSwipeUp; |   final void Function() onSwipeUp; | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart'; | |||||||
| import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart'; | import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart'; | ||||||
| import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'; | import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'; | ||||||
| import 'package:immich_mobile/modules/home/services/asset.service.dart'; | import 'package:immich_mobile/modules/home/services/asset.service.dart'; | ||||||
|  | import 'package:immich_mobile/shared/services/app_settings.service.dart'; | ||||||
| import 'package:openapi/api.dart'; | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| // ignore: must_be_immutable | // ignore: must_be_immutable | ||||||
| @@ -18,8 +19,6 @@ class GalleryViewerPage extends HookConsumerWidget { | |||||||
|   late List<AssetResponseDto> assetList; |   late List<AssetResponseDto> assetList; | ||||||
|   final AssetResponseDto asset; |   final AssetResponseDto asset; | ||||||
|  |  | ||||||
|   static const _threeStageLoading = false; |  | ||||||
|  |  | ||||||
|   GalleryViewerPage({ |   GalleryViewerPage({ | ||||||
|     Key? key, |     Key? key, | ||||||
|     required this.assetList, |     required this.assetList, | ||||||
| @@ -27,21 +26,35 @@ class GalleryViewerPage extends HookConsumerWidget { | |||||||
|   }) : super(key: key); |   }) : super(key: key); | ||||||
|  |  | ||||||
|   AssetResponseDto? assetDetail; |   AssetResponseDto? assetDetail; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     final Box<dynamic> box = Hive.box(userInfoBox); |     final Box<dynamic> box = Hive.box(userInfoBox); | ||||||
|  |     final appSettingService = ref.watch(appSettingsServiceProvider); | ||||||
|  |     final threeStageLoading = useState(false); | ||||||
|  |     final loading = useState(false); | ||||||
|  |     final isZoomed = useState<bool>(false); | ||||||
|  |     ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(false); | ||||||
|  |  | ||||||
|     int indexOfAsset = assetList.indexOf(asset); |     int indexOfAsset = assetList.indexOf(asset); | ||||||
|     final loading = useState(false); |  | ||||||
|  |  | ||||||
|     @override |  | ||||||
|     void initState(int index) { |  | ||||||
|       indexOfAsset = index; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     PageController controller = |     PageController controller = | ||||||
|         PageController(initialPage: assetList.indexOf(asset)); |         PageController(initialPage: assetList.indexOf(asset)); | ||||||
|  |  | ||||||
|  |     useEffect( | ||||||
|  |       () { | ||||||
|  |         threeStageLoading.value = appSettingService | ||||||
|  |             .getSetting<bool>(AppSettingsEnum.threeStageLoading); | ||||||
|  |         return null; | ||||||
|  |       }, | ||||||
|  |       [], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     @override | ||||||
|  |     initState(int index) { | ||||||
|  |       indexOfAsset = index; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     getAssetExif() async { |     getAssetExif() async { | ||||||
|       assetDetail = await ref |       assetDetail = await ref | ||||||
|           .watch(assetServiceProvider) |           .watch(assetServiceProvider) | ||||||
| @@ -60,9 +73,6 @@ class GalleryViewerPage extends HookConsumerWidget { | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     final isZoomed = useState<bool>(false); |  | ||||||
|     ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(false); |  | ||||||
|  |  | ||||||
|     //make isZoomed listener call instead |     //make isZoomed listener call instead | ||||||
|     void isZoomedMethod() { |     void isZoomedMethod() { | ||||||
|       if (isZoomedListener.value) { |       if (isZoomedListener.value) { | ||||||
| @@ -84,7 +94,8 @@ class GalleryViewerPage extends HookConsumerWidget { | |||||||
|           ref |           ref | ||||||
|               .watch(imageViewerStateProvider.notifier) |               .watch(imageViewerStateProvider.notifier) | ||||||
|               .downloadAsset(assetList[indexOfAsset], context); |               .downloadAsset(assetList[indexOfAsset], context); | ||||||
|         }, onSharePressed: () { |         }, | ||||||
|  |         onSharePressed: () { | ||||||
|           ref |           ref | ||||||
|               .watch(imageViewerStateProvider.notifier) |               .watch(imageViewerStateProvider.notifier) | ||||||
|               .shareAsset(assetList[indexOfAsset], context); |               .shareAsset(assetList[indexOfAsset], context); | ||||||
| @@ -101,17 +112,19 @@ class GalleryViewerPage extends HookConsumerWidget { | |||||||
|           scrollDirection: Axis.horizontal, |           scrollDirection: Axis.horizontal, | ||||||
|           itemBuilder: (context, index) { |           itemBuilder: (context, index) { | ||||||
|             initState(index); |             initState(index); | ||||||
|  |  | ||||||
|             getAssetExif(); |             getAssetExif(); | ||||||
|  |  | ||||||
|             if (assetList[index].type == AssetTypeEnum.IMAGE) { |             if (assetList[index].type == AssetTypeEnum.IMAGE) { | ||||||
|               return ImageViewerPage( |               return ImageViewerPage( | ||||||
|                 authToken: 'Bearer ${box.get(accessTokenKey)}', |                 authToken: 'Bearer ${box.get(accessTokenKey)}', | ||||||
|                 isZoomedFunction: isZoomedMethod, |                 isZoomedFunction: isZoomedMethod, | ||||||
|                 isZoomedListener: isZoomedListener, |                 isZoomedListener: isZoomedListener, | ||||||
|                 onLoadingCompleted: () => loading.value = false, |                 onLoadingCompleted: () => {}, | ||||||
|                 onLoadingStart: () => loading.value = _threeStageLoading, |                 onLoadingStart: () => {}, | ||||||
|                 asset: assetList[index], |                 asset: assetList[index], | ||||||
|                 heroTag: assetList[index].id, |                 heroTag: assetList[index].id, | ||||||
|                 threeStageLoading: _threeStageLoading |                 threeStageLoading: threeStageLoading.value, | ||||||
|               ); |               ); | ||||||
|             } else { |             } else { | ||||||
|               return SwipeDetector( |               return SwipeDetector( | ||||||
|   | |||||||
| @@ -35,6 +35,7 @@ class ImageViewerPage extends HookConsumerWidget { | |||||||
|   }) : super(key: key); |   }) : super(key: key); | ||||||
|  |  | ||||||
|   AssetResponseDto? assetDetail; |   AssetResponseDto? assetDetail; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     final downloadAssetStatus = |     final downloadAssetStatus = | ||||||
| @@ -71,18 +72,19 @@ class ImageViewerPage extends HookConsumerWidget { | |||||||
|           child: Hero( |           child: Hero( | ||||||
|             tag: heroTag, |             tag: heroTag, | ||||||
|             child: RemotePhotoView( |             child: RemotePhotoView( | ||||||
|                 thumbnailUrl: getThumbnailUrl(asset), |               thumbnailUrl: getThumbnailUrl(asset), | ||||||
|                 imageUrl: getImageUrl(asset), |               imageUrl: getImageUrl(asset), | ||||||
|                 previewUrl: threeStageLoading |               previewUrl: threeStageLoading | ||||||
|                     ? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG) |                   ? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG) | ||||||
|                     : null, |                   : null, | ||||||
|                 authToken: authToken, |               authToken: authToken, | ||||||
|                 isZoomedFunction: isZoomedFunction, |               isZoomedFunction: isZoomedFunction, | ||||||
|                 isZoomedListener: isZoomedListener, |               isZoomedListener: isZoomedListener, | ||||||
|                 onSwipeDown: () => AutoRouter.of(context).pop(), |               onSwipeDown: () => AutoRouter.of(context).pop(), | ||||||
|                 onSwipeUp: () => showInfo(), |               onSwipeUp: () => showInfo(), | ||||||
|                 onLoadingCompleted: onLoadingCompleted, |               onLoadingCompleted: onLoadingCompleted, | ||||||
|                 onLoadingStart: onLoadingStart), |               onLoadingStart: onLoadingStart, | ||||||
|  |             ), | ||||||
|           ), |           ), | ||||||
|         ), |         ), | ||||||
|         if (downloadAssetStatus == DownloadAssetStatus.loading) |         if (downloadAssetStatus == DownloadAssetStatus.loading) | ||||||
|   | |||||||
| @@ -1,303 +0,0 @@ | |||||||
| 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:hive_flutter/hive_flutter.dart'; |  | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; |  | ||||||
| import 'package:image_picker/image_picker.dart'; |  | ||||||
| import 'package:immich_mobile/constants/hive_box.dart'; |  | ||||||
| import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart'; |  | ||||||
| import 'package:immich_mobile/routing/router.dart'; |  | ||||||
| import 'package:immich_mobile/shared/providers/asset.provider.dart'; |  | ||||||
| import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; |  | ||||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; |  | ||||||
| import 'package:immich_mobile/shared/models/server_info_state.model.dart'; |  | ||||||
| import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; |  | ||||||
| import 'package:immich_mobile/shared/providers/server_info.provider.dart'; |  | ||||||
| import 'package:immich_mobile/shared/providers/websocket.provider.dart'; |  | ||||||
| import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; |  | ||||||
| import 'package:package_info_plus/package_info_plus.dart'; |  | ||||||
| import 'dart:math'; |  | ||||||
|  |  | ||||||
| class ProfileDrawer extends HookConsumerWidget { |  | ||||||
|   const ProfileDrawer({Key? key}) : super(key: key); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |  | ||||||
|     String endpoint = Hive.box(userInfoBox).get(serverEndpointKey); |  | ||||||
|     AuthenticationState authState = ref.watch(authenticationProvider); |  | ||||||
|     ServerInfoState serverInfoState = ref.watch(serverInfoProvider); |  | ||||||
|     final uploadProfileImageStatus = |  | ||||||
|         ref.watch(uploadProfileImageProvider).status; |  | ||||||
|     final appInfo = useState({}); |  | ||||||
|     var dummmy = Random().nextInt(1024); |  | ||||||
|  |  | ||||||
|     _getPackageInfo() async { |  | ||||||
|       PackageInfo packageInfo = await PackageInfo.fromPlatform(); |  | ||||||
|  |  | ||||||
|       appInfo.value = { |  | ||||||
|         "version": packageInfo.version, |  | ||||||
|         "buildNumber": packageInfo.buildNumber, |  | ||||||
|       }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     _buildUserProfileImage() { |  | ||||||
|       if (authState.profileImagePath.isEmpty) { |  | ||||||
|         return const CircleAvatar( |  | ||||||
|           radius: 35, |  | ||||||
|           backgroundImage: AssetImage('assets/immich-logo-no-outline.png'), |  | ||||||
|           backgroundColor: Colors.transparent, |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (uploadProfileImageStatus == UploadProfileStatus.idle) { |  | ||||||
|         if (authState.profileImagePath.isNotEmpty) { |  | ||||||
|           return CircleAvatar( |  | ||||||
|             radius: 35, |  | ||||||
|             backgroundImage: NetworkImage( |  | ||||||
|               '$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}', |  | ||||||
|             ), |  | ||||||
|             backgroundColor: Colors.transparent, |  | ||||||
|           ); |  | ||||||
|         } else { |  | ||||||
|           return const CircleAvatar( |  | ||||||
|             radius: 35, |  | ||||||
|             backgroundImage: AssetImage('assets/immich-logo-no-outline.png'), |  | ||||||
|             backgroundColor: Colors.transparent, |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (uploadProfileImageStatus == UploadProfileStatus.success) { |  | ||||||
|         return CircleAvatar( |  | ||||||
|           radius: 35, |  | ||||||
|           backgroundImage: NetworkImage( |  | ||||||
|             '$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}', |  | ||||||
|           ), |  | ||||||
|           backgroundColor: Colors.transparent, |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (uploadProfileImageStatus == UploadProfileStatus.failure) { |  | ||||||
|         return const CircleAvatar( |  | ||||||
|           radius: 35, |  | ||||||
|           backgroundImage: AssetImage('assets/immich-logo-no-outline.png'), |  | ||||||
|           backgroundColor: Colors.transparent, |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (uploadProfileImageStatus == UploadProfileStatus.loading) { |  | ||||||
|         return const ImmichLoadingIndicator(); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       return const SizedBox(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     _pickUserProfileImage() async { |  | ||||||
|       final XFile? image = await ImagePicker().pickImage( |  | ||||||
|         source: ImageSource.gallery, |  | ||||||
|         maxHeight: 1024, |  | ||||||
|         maxWidth: 1024, |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       if (image != null) { |  | ||||||
|         var success = |  | ||||||
|             await ref.watch(uploadProfileImageProvider.notifier).upload(image); |  | ||||||
|  |  | ||||||
|         if (success) { |  | ||||||
|           ref.watch(authenticationProvider.notifier).updateUserProfileImagePath( |  | ||||||
|                 ref.read(uploadProfileImageProvider).profileImagePath, |  | ||||||
|               ); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     useEffect( |  | ||||||
|       () { |  | ||||||
|         _getPackageInfo(); |  | ||||||
|         _buildUserProfileImage(); |  | ||||||
|         return null; |  | ||||||
|       }, |  | ||||||
|       [], |  | ||||||
|     ); |  | ||||||
|     return Drawer( |  | ||||||
|       child: Column( |  | ||||||
|         mainAxisAlignment: MainAxisAlignment.spaceBetween, |  | ||||||
|         children: [ |  | ||||||
|           ListView( |  | ||||||
|             shrinkWrap: true, |  | ||||||
|             padding: EdgeInsets.zero, |  | ||||||
|             children: [ |  | ||||||
|               DrawerHeader( |  | ||||||
|                 decoration: const BoxDecoration( |  | ||||||
|                   gradient: LinearGradient( |  | ||||||
|                     colors: [ |  | ||||||
|                       Color.fromARGB(255, 216, 219, 238), |  | ||||||
|                       Color.fromARGB(255, 226, 230, 231) |  | ||||||
|                     ], |  | ||||||
|                     begin: Alignment.centerRight, |  | ||||||
|                     end: Alignment.centerLeft, |  | ||||||
|                   ), |  | ||||||
|                 ), |  | ||||||
|                 child: Column( |  | ||||||
|                   mainAxisAlignment: MainAxisAlignment.start, |  | ||||||
|                   crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|                   children: [ |  | ||||||
|                     Stack( |  | ||||||
|                       clipBehavior: Clip.none, |  | ||||||
|                       children: [ |  | ||||||
|                         _buildUserProfileImage(), |  | ||||||
|                         Positioned( |  | ||||||
|                           bottom: 0, |  | ||||||
|                           right: -5, |  | ||||||
|                           child: GestureDetector( |  | ||||||
|                             onTap: _pickUserProfileImage, |  | ||||||
|                             child: Material( |  | ||||||
|                               color: Colors.grey[50], |  | ||||||
|                               elevation: 2, |  | ||||||
|                               shape: RoundedRectangleBorder( |  | ||||||
|                                 borderRadius: BorderRadius.circular(50.0), |  | ||||||
|                               ), |  | ||||||
|                               child: Padding( |  | ||||||
|                                 padding: const EdgeInsets.all(5.0), |  | ||||||
|                                 child: Icon( |  | ||||||
|                                   Icons.edit, |  | ||||||
|                                   color: Theme.of(context).primaryColor, |  | ||||||
|                                   size: 14, |  | ||||||
|                                 ), |  | ||||||
|                               ), |  | ||||||
|                             ), |  | ||||||
|                           ), |  | ||||||
|                         ), |  | ||||||
|                       ], |  | ||||||
|                     ), |  | ||||||
|                     Text( |  | ||||||
|                       "${authState.firstName} ${authState.lastName}", |  | ||||||
|                       style: TextStyle( |  | ||||||
|                         color: Theme.of(context).primaryColor, |  | ||||||
|                         fontWeight: FontWeight.bold, |  | ||||||
|                         fontSize: 24, |  | ||||||
|                       ), |  | ||||||
|                     ), |  | ||||||
|                     Text( |  | ||||||
|                       authState.userEmail, |  | ||||||
|                       style: TextStyle(color: Colors.grey[800], fontSize: 12), |  | ||||||
|                     ) |  | ||||||
|                   ], |  | ||||||
|                 ), |  | ||||||
|               ), |  | ||||||
|               ListTile( |  | ||||||
|                 tileColor: Colors.grey[100], |  | ||||||
|                 leading: const Icon( |  | ||||||
|                   Icons.logout_rounded, |  | ||||||
|                   color: Colors.black54, |  | ||||||
|                 ), |  | ||||||
|                 title: const Text( |  | ||||||
|                   "profile_drawer_sign_out", |  | ||||||
|                   style: TextStyle( |  | ||||||
|                     color: Colors.black54, |  | ||||||
|                     fontSize: 14, |  | ||||||
|                     fontWeight: FontWeight.bold, |  | ||||||
|                   ), |  | ||||||
|                 ).tr(), |  | ||||||
|                 onTap: () async { |  | ||||||
|                   bool res = |  | ||||||
|                       await ref.watch(authenticationProvider.notifier).logout(); |  | ||||||
|  |  | ||||||
|                   if (res) { |  | ||||||
|                     ref.watch(backupProvider.notifier).cancelBackup(); |  | ||||||
|                     ref.watch(assetProvider.notifier).clearAllAsset(); |  | ||||||
|                     ref.watch(websocketProvider.notifier).disconnect(); |  | ||||||
|                     // AutoRouter.of(context).popUntilRoot(); |  | ||||||
|                     AutoRouter.of(context).replace(const LoginRoute()); |  | ||||||
|                   } |  | ||||||
|                 }, |  | ||||||
|               ) |  | ||||||
|             ], |  | ||||||
|           ), |  | ||||||
|           Padding( |  | ||||||
|             padding: const EdgeInsets.all(8.0), |  | ||||||
|             child: Card( |  | ||||||
|               elevation: 0, |  | ||||||
|               color: Colors.grey[100], |  | ||||||
|               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(), |  | ||||||
|                     Row( |  | ||||||
|                       mainAxisAlignment: MainAxisAlignment.spaceBetween, |  | ||||||
|                       children: [ |  | ||||||
|                         Text( |  | ||||||
|                           "App Version", |  | ||||||
|                           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(), |  | ||||||
|                     Row( |  | ||||||
|                       mainAxisAlignment: MainAxisAlignment.spaceBetween, |  | ||||||
|                       children: [ |  | ||||||
|                         Text( |  | ||||||
|                           "Server Version", |  | ||||||
|                           style: TextStyle( |  | ||||||
|                             fontSize: 11, |  | ||||||
|                             color: Colors.grey[500], |  | ||||||
|                             fontWeight: FontWeight.bold, |  | ||||||
|                           ), |  | ||||||
|                         ), |  | ||||||
|                         Text( |  | ||||||
|                           "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch_}", |  | ||||||
|                           style: TextStyle( |  | ||||||
|                             fontSize: 11, |  | ||||||
|                             color: Colors.grey[500], |  | ||||||
|                             fontWeight: FontWeight.bold, |  | ||||||
|                           ), |  | ||||||
|                         ), |  | ||||||
|                       ], |  | ||||||
|                     ), |  | ||||||
|                   ], |  | ||||||
|                 ), |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|           ) |  | ||||||
|         ], |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,93 @@ | |||||||
|  | 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/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/routing/router.dart'; | ||||||
|  | import 'package:immich_mobile/shared/providers/asset.provider.dart'; | ||||||
|  | import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||||
|  | import 'package:immich_mobile/modules/backup/providers/backup.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) { | ||||||
|  |     _buildSignoutButton() { | ||||||
|  |       return ListTile( | ||||||
|  |         horizontalTitleGap: 0, | ||||||
|  |         leading: SizedBox( | ||||||
|  |           height: double.infinity, | ||||||
|  |           child: Icon( | ||||||
|  |             Icons.logout_rounded, | ||||||
|  |             color: Colors.grey[700], | ||||||
|  |             size: 20, | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |         title: Text( | ||||||
|  |           "profile_drawer_sign_out", | ||||||
|  |           style: TextStyle( | ||||||
|  |             color: Colors.grey[700], | ||||||
|  |             fontSize: 12, | ||||||
|  |             fontWeight: FontWeight.bold, | ||||||
|  |           ), | ||||||
|  |         ).tr(), | ||||||
|  |         onTap: () async { | ||||||
|  |           bool res = await ref.watch(authenticationProvider.notifier).logout(); | ||||||
|  |  | ||||||
|  |           if (res) { | ||||||
|  |             ref.watch(backupProvider.notifier).cancelBackup(); | ||||||
|  |             ref.watch(assetProvider.notifier).clearAllAsset(); | ||||||
|  |             ref.watch(websocketProvider.notifier).disconnect(); | ||||||
|  |             AutoRouter.of(context).replace(const LoginRoute()); | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     _buildSettingButton() { | ||||||
|  |       return ListTile( | ||||||
|  |         horizontalTitleGap: 0, | ||||||
|  |         leading: SizedBox( | ||||||
|  |           height: double.infinity, | ||||||
|  |           child: Icon( | ||||||
|  |             Icons.settings_rounded, | ||||||
|  |             color: Colors.grey[700], | ||||||
|  |             size: 20, | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |         title: Text( | ||||||
|  |           "profile_drawer_settings", | ||||||
|  |           style: TextStyle( | ||||||
|  |             color: Colors.grey[700], | ||||||
|  |             fontSize: 12, | ||||||
|  |             fontWeight: FontWeight.bold, | ||||||
|  |           ), | ||||||
|  |         ).tr(), | ||||||
|  |         onTap: () { | ||||||
|  |           AutoRouter.of(context).push(const SettingsRoute()); | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return Drawer( | ||||||
|  |       child: Column( | ||||||
|  |         mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
|  |         children: [ | ||||||
|  |           ListView( | ||||||
|  |             shrinkWrap: true, | ||||||
|  |             padding: EdgeInsets.zero, | ||||||
|  |             children: [ | ||||||
|  |               const ProfileDrawerHeader(), | ||||||
|  |               _buildSettingButton(), | ||||||
|  |               _buildSignoutButton(), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |           const ServerInfoBox() | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -0,0 +1,166 @@ | |||||||
|  | import 'dart:math'; | ||||||
|  |  | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
|  | import 'package:hive_flutter/hive_flutter.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:image_picker/image_picker.dart'; | ||||||
|  | import 'package:immich_mobile/constants/hive_box.dart'; | ||||||
|  | import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart'; | ||||||
|  | import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; | ||||||
|  | import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||||
|  | import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; | ||||||
|  |  | ||||||
|  | class ProfileDrawerHeader extends HookConsumerWidget { | ||||||
|  |   const ProfileDrawerHeader({ | ||||||
|  |     Key? key, | ||||||
|  |   }) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     String endpoint = Hive.box(userInfoBox).get(serverEndpointKey); | ||||||
|  |     AuthenticationState authState = ref.watch(authenticationProvider); | ||||||
|  |     final uploadProfileImageStatus = | ||||||
|  |         ref.watch(uploadProfileImageProvider).status; | ||||||
|  |     var dummmy = Random().nextInt(1024); | ||||||
|  |  | ||||||
|  |     _buildUserProfileImage() { | ||||||
|  |       if (authState.profileImagePath.isEmpty) { | ||||||
|  |         return const CircleAvatar( | ||||||
|  |           radius: 35, | ||||||
|  |           backgroundImage: AssetImage('assets/immich-logo-no-outline.png'), | ||||||
|  |           backgroundColor: Colors.transparent, | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (uploadProfileImageStatus == UploadProfileStatus.idle) { | ||||||
|  |         if (authState.profileImagePath.isNotEmpty) { | ||||||
|  |           return CircleAvatar( | ||||||
|  |             radius: 35, | ||||||
|  |             backgroundImage: NetworkImage( | ||||||
|  |               '$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}', | ||||||
|  |             ), | ||||||
|  |             backgroundColor: Colors.transparent, | ||||||
|  |           ); | ||||||
|  |         } else { | ||||||
|  |           return const CircleAvatar( | ||||||
|  |             radius: 35, | ||||||
|  |             backgroundImage: AssetImage('assets/immich-logo-no-outline.png'), | ||||||
|  |             backgroundColor: Colors.transparent, | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (uploadProfileImageStatus == UploadProfileStatus.success) { | ||||||
|  |         return CircleAvatar( | ||||||
|  |           radius: 35, | ||||||
|  |           backgroundImage: NetworkImage( | ||||||
|  |             '$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}', | ||||||
|  |           ), | ||||||
|  |           backgroundColor: Colors.transparent, | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (uploadProfileImageStatus == UploadProfileStatus.failure) { | ||||||
|  |         return const CircleAvatar( | ||||||
|  |           radius: 35, | ||||||
|  |           backgroundImage: AssetImage('assets/immich-logo-no-outline.png'), | ||||||
|  |           backgroundColor: Colors.transparent, | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (uploadProfileImageStatus == UploadProfileStatus.loading) { | ||||||
|  |         return const ImmichLoadingIndicator(); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return const SizedBox(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     _pickUserProfileImage() async { | ||||||
|  |       final XFile? image = await ImagePicker().pickImage( | ||||||
|  |         source: ImageSource.gallery, | ||||||
|  |         maxHeight: 1024, | ||||||
|  |         maxWidth: 1024, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       if (image != null) { | ||||||
|  |         var success = | ||||||
|  |             await ref.watch(uploadProfileImageProvider.notifier).upload(image); | ||||||
|  |  | ||||||
|  |         if (success) { | ||||||
|  |           ref.watch(authenticationProvider.notifier).updateUserProfileImagePath( | ||||||
|  |                 ref.read(uploadProfileImageProvider).profileImagePath, | ||||||
|  |               ); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     useEffect( | ||||||
|  |       () { | ||||||
|  |         _buildUserProfileImage(); | ||||||
|  |         return null; | ||||||
|  |       }, | ||||||
|  |       [], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     return DrawerHeader( | ||||||
|  |       decoration: const BoxDecoration( | ||||||
|  |         gradient: LinearGradient( | ||||||
|  |           colors: [ | ||||||
|  |             Color.fromARGB(255, 216, 219, 238), | ||||||
|  |             Color.fromARGB(255, 242, 242, 242), | ||||||
|  |             Colors.white, | ||||||
|  |           ], | ||||||
|  |           begin: Alignment.centerRight, | ||||||
|  |           end: Alignment.centerLeft, | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |       child: Column( | ||||||
|  |         mainAxisAlignment: MainAxisAlignment.start, | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |         children: [ | ||||||
|  |           Stack( | ||||||
|  |             clipBehavior: Clip.none, | ||||||
|  |             children: [ | ||||||
|  |               _buildUserProfileImage(), | ||||||
|  |               Positioned( | ||||||
|  |                 bottom: 0, | ||||||
|  |                 right: -5, | ||||||
|  |                 child: GestureDetector( | ||||||
|  |                   onTap: _pickUserProfileImage, | ||||||
|  |                   child: Material( | ||||||
|  |                     color: Colors.grey[50], | ||||||
|  |                     elevation: 2, | ||||||
|  |                     shape: RoundedRectangleBorder( | ||||||
|  |                       borderRadius: BorderRadius.circular(50.0), | ||||||
|  |                     ), | ||||||
|  |                     child: Padding( | ||||||
|  |                       padding: const EdgeInsets.all(5.0), | ||||||
|  |                       child: Icon( | ||||||
|  |                         Icons.edit, | ||||||
|  |                         color: Theme.of(context).primaryColor, | ||||||
|  |                         size: 14, | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |           Text( | ||||||
|  |             "${authState.firstName} ${authState.lastName}", | ||||||
|  |             style: TextStyle( | ||||||
|  |               color: Theme.of(context).primaryColor, | ||||||
|  |               fontWeight: FontWeight.bold, | ||||||
|  |               fontSize: 24, | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |           Text( | ||||||
|  |             authState.userEmail, | ||||||
|  |             style: TextStyle(color: Colors.grey[800], fontSize: 12), | ||||||
|  |           ) | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										118
									
								
								mobile/lib/modules/home/ui/profile_drawer/server_info_box.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								mobile/lib/modules/home/ui/profile_drawer/server_info_box.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | |||||||
|  | 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_state.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) { | ||||||
|  |     ServerInfoState 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: Colors.grey[100], | ||||||
|  |         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(), | ||||||
|  |               Row( | ||||||
|  |                 mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
|  |                 children: [ | ||||||
|  |                   Text( | ||||||
|  |                     "App Version", | ||||||
|  |                     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(), | ||||||
|  |               Row( | ||||||
|  |                 mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
|  |                 children: [ | ||||||
|  |                   Text( | ||||||
|  |                     "Server Version", | ||||||
|  |                     style: TextStyle( | ||||||
|  |                       fontSize: 11, | ||||||
|  |                       color: Colors.grey[500], | ||||||
|  |                       fontWeight: FontWeight.bold, | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                   Text( | ||||||
|  |                     "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch_}", | ||||||
|  |                     style: TextStyle( | ||||||
|  |                       fontSize: 11, | ||||||
|  |                       color: Colors.grey[500], | ||||||
|  |                       fontWeight: FontWeight.bold, | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -9,7 +9,7 @@ import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart'; | |||||||
| import 'package:immich_mobile/modules/home/ui/image_grid.dart'; | import 'package:immich_mobile/modules/home/ui/image_grid.dart'; | ||||||
| import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart'; | import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart'; | ||||||
| import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart'; | import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart'; | ||||||
| import 'package:immich_mobile/modules/home/ui/profile_drawer.dart'; | import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart'; | ||||||
|  |  | ||||||
| import 'package:immich_mobile/shared/providers/asset.provider.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/server_info.provider.dart'; | ||||||
|   | |||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/three_stage_loading.dart'; | ||||||
|  |  | ||||||
|  | class ImageViewerQualitySetting extends StatelessWidget { | ||||||
|  |   const ImageViewerQualitySetting({ | ||||||
|  |     Key? key, | ||||||
|  |   }) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return const ExpansionTile( | ||||||
|  |       title: Text( | ||||||
|  |         'Image viewer quality', | ||||||
|  |         style: TextStyle( | ||||||
|  |           fontWeight: FontWeight.bold, | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |       subtitle: Text( | ||||||
|  |         'Adjust the quality of the detail image viewer', | ||||||
|  |         style: TextStyle( | ||||||
|  |           fontSize: 13, | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |       children: [ | ||||||
|  |         ThreeStageLoading(), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -0,0 +1,54 @@ | |||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:immich_mobile/shared/services/app_settings.service.dart'; | ||||||
|  |  | ||||||
|  | class ThreeStageLoading extends HookConsumerWidget { | ||||||
|  |   const ThreeStageLoading({ | ||||||
|  |     Key? key, | ||||||
|  |   }) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     final appSettingService = ref.watch(appSettingsServiceProvider); | ||||||
|  |  | ||||||
|  |     final isEnable = useState(false); | ||||||
|  |  | ||||||
|  |     useEffect( | ||||||
|  |       () { | ||||||
|  |         var isThreeStageLoadingEnable = | ||||||
|  |             appSettingService.getSetting(AppSettingsEnum.threeStageLoading); | ||||||
|  |  | ||||||
|  |         isEnable.value = isThreeStageLoadingEnable; | ||||||
|  |         return null; | ||||||
|  |       }, | ||||||
|  |       [], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     void onSwitchChanged(bool switchValue) { | ||||||
|  |       appSettingService.setSetting( | ||||||
|  |         AppSettingsEnum.threeStageLoading, | ||||||
|  |         switchValue, | ||||||
|  |       ); | ||||||
|  |       isEnable.value = switchValue; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return SwitchListTile.adaptive( | ||||||
|  |       title: const Text( | ||||||
|  |         "Enable three stage loading", | ||||||
|  |         style: TextStyle( | ||||||
|  |           fontSize: 12, | ||||||
|  |           fontWeight: FontWeight.bold, | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |       subtitle: const Text( | ||||||
|  |         "The three-stage loading delivers the best quality image in exchange for a slower loading speed", | ||||||
|  |         style: TextStyle( | ||||||
|  |           fontSize: 12, | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |       value: isEnable.value, | ||||||
|  |       onChanged: onSwitchChanged, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										81
									
								
								mobile/lib/modules/settings/views/settings_page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								mobile/lib/modules/settings/views/settings_page.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | |||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart'; | ||||||
|  |  | ||||||
|  | class SettingsPage extends HookConsumerWidget { | ||||||
|  |   const SettingsPage({Key? key}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     return Scaffold( | ||||||
|  |       appBar: AppBar( | ||||||
|  |         leading: IconButton( | ||||||
|  |           iconSize: 20, | ||||||
|  |           splashRadius: 24, | ||||||
|  |           onPressed: () { | ||||||
|  |             Navigator.pop(context); | ||||||
|  |           }, | ||||||
|  |           icon: const Icon(Icons.arrow_back_ios_new_rounded), | ||||||
|  |         ), | ||||||
|  |         automaticallyImplyLeading: false, | ||||||
|  |         centerTitle: false, | ||||||
|  |         title: const Text( | ||||||
|  |           'Settings', | ||||||
|  |           style: TextStyle( | ||||||
|  |             fontSize: 16, | ||||||
|  |             fontWeight: FontWeight.bold, | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |       body: ListView( | ||||||
|  |         children: [ | ||||||
|  |           ...ListTile.divideTiles( | ||||||
|  |             context: context, | ||||||
|  |             tiles: [ | ||||||
|  |               const ImageViewerQualitySetting(), | ||||||
|  |               const SettingListTile( | ||||||
|  |                 title: 'Theme', | ||||||
|  |                 subtitle: 'Choose between light and dark theme', | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ).toList(), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class SettingListTile extends StatelessWidget { | ||||||
|  |   const SettingListTile({ | ||||||
|  |     required this.title, | ||||||
|  |     required this.subtitle, | ||||||
|  |     Key? key, | ||||||
|  |   }) : super(key: key); | ||||||
|  |  | ||||||
|  |   final String title; | ||||||
|  |   final String subtitle; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return ListTile( | ||||||
|  |       dense: true, | ||||||
|  |       title: Text( | ||||||
|  |         title, | ||||||
|  |         style: const TextStyle( | ||||||
|  |           fontWeight: FontWeight.bold, | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |       subtitle: Text( | ||||||
|  |         subtitle, | ||||||
|  |         style: const TextStyle( | ||||||
|  |           fontSize: 12, | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |       trailing: const Icon( | ||||||
|  |         Icons.keyboard_arrow_right_rounded, | ||||||
|  |         size: 24, | ||||||
|  |       ), | ||||||
|  |       onTap: () {}, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -18,6 +18,7 @@ import 'package:immich_mobile/modules/album/views/create_album_page.dart'; | |||||||
| import 'package:immich_mobile/modules/album/views/select_additional_user_for_sharing_page.dart'; | import 'package:immich_mobile/modules/album/views/select_additional_user_for_sharing_page.dart'; | ||||||
| import 'package:immich_mobile/modules/album/views/select_user_for_sharing_page.dart'; | import 'package:immich_mobile/modules/album/views/select_user_for_sharing_page.dart'; | ||||||
| import 'package:immich_mobile/modules/album/views/sharing_page.dart'; | import 'package:immich_mobile/modules/album/views/sharing_page.dart'; | ||||||
|  | import 'package:immich_mobile/modules/settings/views/settings_page.dart'; | ||||||
| import 'package:immich_mobile/routing/auth_guard.dart'; | import 'package:immich_mobile/routing/auth_guard.dart'; | ||||||
| import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart'; | import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart'; | ||||||
| import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart'; | import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart'; | ||||||
| @@ -77,6 +78,7 @@ part 'router.gr.dart'; | |||||||
|       guards: [AuthGuard], |       guards: [AuthGuard], | ||||||
|       transitionsBuilder: TransitionsBuilders.slideBottom, |       transitionsBuilder: TransitionsBuilders.slideBottom, | ||||||
|     ), |     ), | ||||||
|  |     AutoRoute(page: SettingsPage, guards: [AuthGuard]), | ||||||
|   ], |   ], | ||||||
| ) | ) | ||||||
| class AppRouter extends _$AppRouter { | class AppRouter extends _$AppRouter { | ||||||
|   | |||||||
| @@ -137,6 +137,10 @@ class _$AppRouter extends RootStackRouter { | |||||||
|           opaque: true, |           opaque: true, | ||||||
|           barrierDismissible: false); |           barrierDismissible: false); | ||||||
|     }, |     }, | ||||||
|  |     SettingsRoute.name: (routeData) { | ||||||
|  |       return MaterialPageX<dynamic>( | ||||||
|  |           routeData: routeData, child: const SettingsPage()); | ||||||
|  |     }, | ||||||
|     HomeRoute.name: (routeData) { |     HomeRoute.name: (routeData) { | ||||||
|       return MaterialPageX<dynamic>( |       return MaterialPageX<dynamic>( | ||||||
|           routeData: routeData, child: const HomePage()); |           routeData: routeData, child: const HomePage()); | ||||||
| @@ -211,7 +215,9 @@ class _$AppRouter extends RootStackRouter { | |||||||
|         RouteConfig(AlbumPreviewRoute.name, |         RouteConfig(AlbumPreviewRoute.name, | ||||||
|             path: '/album-preview-page', guards: [authGuard]), |             path: '/album-preview-page', guards: [authGuard]), | ||||||
|         RouteConfig(FailedBackupStatusRoute.name, |         RouteConfig(FailedBackupStatusRoute.name, | ||||||
|             path: '/failed-backup-status-page', guards: [authGuard]) |             path: '/failed-backup-status-page', guards: [authGuard]), | ||||||
|  |         RouteConfig(SettingsRoute.name, | ||||||
|  |             path: '/settings-page', guards: [authGuard]) | ||||||
|       ]; |       ]; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -546,6 +552,14 @@ class FailedBackupStatusRoute extends PageRouteInfo<void> { | |||||||
|   static const String name = 'FailedBackupStatusRoute'; |   static const String name = 'FailedBackupStatusRoute'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /// generated route for | ||||||
|  | /// [SettingsPage] | ||||||
|  | class SettingsRoute extends PageRouteInfo<void> { | ||||||
|  |   const SettingsRoute() : super(SettingsRoute.name, path: '/settings-page'); | ||||||
|  |  | ||||||
|  |   static const String name = 'SettingsRoute'; | ||||||
|  | } | ||||||
|  |  | ||||||
| /// generated route for | /// generated route for | ||||||
| /// [HomePage] | /// [HomePage] | ||||||
| class HomeRoute extends PageRouteInfo<void> { | class HomeRoute extends PageRouteInfo<void> { | ||||||
|   | |||||||
							
								
								
									
										79
									
								
								mobile/lib/shared/services/app_settings.service.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								mobile/lib/shared/services/app_settings.service.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | |||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:hive_flutter/hive_flutter.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:immich_mobile/constants/hive_box.dart'; | ||||||
|  |  | ||||||
|  | enum AppSettingsEnum { | ||||||
|  |   threeStageLoading, // true, false, | ||||||
|  |   themeMode, // "light","dark" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class AppSettingsService { | ||||||
|  |   late final Box hiveBox; | ||||||
|  |  | ||||||
|  |   AppSettingsService() { | ||||||
|  |     hiveBox = Hive.box(userSettingInfoBox); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   T getSetting<T>(AppSettingsEnum settingType) { | ||||||
|  |     var settingKey = _settingHiveBoxKeyLookup(settingType); | ||||||
|  |  | ||||||
|  |     if (!hiveBox.containsKey(settingKey)) { | ||||||
|  |       T defaultSetting = _setDefaultSetting(settingType); | ||||||
|  |       return defaultSetting; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     var result = hiveBox.get(settingKey); | ||||||
|  |  | ||||||
|  |     if (result is T) { | ||||||
|  |       return result; | ||||||
|  |     } else { | ||||||
|  |       debugPrint("Incorrect setting type"); | ||||||
|  |       throw TypeError(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   setSetting<T>(AppSettingsEnum settingType, T value) { | ||||||
|  |     var settingKey = _settingHiveBoxKeyLookup(settingType); | ||||||
|  |  | ||||||
|  |     if (hiveBox.containsKey(settingKey)) { | ||||||
|  |       var result = hiveBox.get(settingKey); | ||||||
|  |  | ||||||
|  |       if (result is! T) { | ||||||
|  |         debugPrint("Incorrect setting type"); | ||||||
|  |         throw TypeError(); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       hiveBox.put(settingKey, value); | ||||||
|  |     } else { | ||||||
|  |       hiveBox.put(settingKey, value); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   _setDefaultSetting(AppSettingsEnum settingType) { | ||||||
|  |     var settingKey = _settingHiveBoxKeyLookup(settingType); | ||||||
|  |  | ||||||
|  |     // Default value of threeStageLoading is false | ||||||
|  |     if (settingType == AppSettingsEnum.threeStageLoading) { | ||||||
|  |       hiveBox.put(settingKey, false); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Default value of themeMode is "light" | ||||||
|  |     if (settingType == AppSettingsEnum.themeMode) { | ||||||
|  |       hiveBox.put(settingKey, "light"); | ||||||
|  |       return "light"; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   String _settingHiveBoxKeyLookup(AppSettingsEnum settingType) { | ||||||
|  |     switch (settingType) { | ||||||
|  |       case AppSettingsEnum.threeStageLoading: | ||||||
|  |         return 'threeStageLoading'; | ||||||
|  |       case AppSettingsEnum.themeMode: | ||||||
|  |         return 'themeMode'; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | final appSettingsServiceProvider = Provider((ref) => AppSettingsService()); | ||||||
| @@ -53,21 +53,23 @@ class TabControllerPage extends ConsumerWidget { | |||||||
|                     items: [ |                     items: [ | ||||||
|                       BottomNavigationBarItem( |                       BottomNavigationBarItem( | ||||||
|                         label: 'tab_controller_nav_photos'.tr(), |                         label: 'tab_controller_nav_photos'.tr(), | ||||||
|                         icon: const Icon(Icons.photo), |                         icon: const Icon(Icons.photo_outlined), | ||||||
|  |                         activeIcon: const Icon(Icons.photo), | ||||||
|                       ), |                       ), | ||||||
|                       BottomNavigationBarItem( |                       BottomNavigationBarItem( | ||||||
|                         label: 'tab_controller_nav_search'.tr(), |                         label: 'tab_controller_nav_search'.tr(), | ||||||
|                         icon: const Icon(Icons.search), |                         icon: const Icon(Icons.search_rounded), | ||||||
|  |                         activeIcon: const Icon(Icons.search), | ||||||
|                       ), |                       ), | ||||||
|                       BottomNavigationBarItem( |                       BottomNavigationBarItem( | ||||||
|                         label: 'tab_controller_nav_sharing'.tr(), |                         label: 'tab_controller_nav_sharing'.tr(), | ||||||
|                         icon: const Icon(Icons.group_outlined), |                         icon: const Icon(Icons.group_outlined), | ||||||
|  |                         activeIcon: const Icon(Icons.group), | ||||||
|                       ), |                       ), | ||||||
|                       BottomNavigationBarItem( |                       BottomNavigationBarItem( | ||||||
|                         label: 'tab_controller_nav_library'.tr(), |                         label: 'tab_controller_nav_library'.tr(), | ||||||
|                         icon: const Icon( |                         icon: const Icon(Icons.photo_album_outlined), | ||||||
|                           Icons.photo_album_outlined, |                         activeIcon: const Icon(Icons.photo_album_rounded), | ||||||
|                         ), |  | ||||||
|                       ) |                       ) | ||||||
|                     ], |                     ], | ||||||
|                   ), |                   ), | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user