You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02: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", | ||||
|   "monthly_title_text_date_format": "MMMM y", | ||||
|   "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_page_no_objects": "No Objects Info Available", | ||||
|   "search_page_no_places": "No Places Info Available", | ||||
|   | ||||
| @@ -16,3 +16,6 @@ const String backupInfoKey = "immichBackupAlbumInfoKey"; // Key 1 | ||||
| // Github Release Info | ||||
| const String hiveGithubReleaseInfoBox = "immichGithubReleaseInfoBox"; // Box | ||||
| 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<HiveBackupAlbums>(hiveBackupInfoBox); | ||||
|   await Hive.openBox(hiveGithubReleaseInfoBox); | ||||
|   await Hive.openBox(userSettingInfoBox); | ||||
|  | ||||
|   SystemChrome.setSystemUIOverlayStyle( | ||||
|     const SystemUiOverlayStyle( | ||||
|   | ||||
| @@ -56,11 +56,11 @@ class _RemotePhotoViewState extends State<RemotePhotoView> { | ||||
|   } | ||||
|  | ||||
|   void _fireStartLoadingEvent() { | ||||
|     if (widget.onLoadingStart != null) widget.onLoadingStart!(); | ||||
|     widget.onLoadingStart(); | ||||
|   } | ||||
|  | ||||
|   void _fireFinishedLoadingEvent() { | ||||
|     if (widget.onLoadingCompleted != null) widget.onLoadingCompleted!(); | ||||
|     widget.onLoadingCompleted(); | ||||
|   } | ||||
|  | ||||
|   CachedNetworkImageProvider _authorizedImageProvider(String url) { | ||||
| @@ -141,26 +141,26 @@ class _RemotePhotoViewState extends State<RemotePhotoView> { | ||||
| } | ||||
|  | ||||
| class RemotePhotoView extends StatefulWidget { | ||||
|   const RemotePhotoView( | ||||
|       {Key? key, | ||||
|       required this.thumbnailUrl, | ||||
|       required this.imageUrl, | ||||
|       required this.authToken, | ||||
|       required this.isZoomedFunction, | ||||
|       required this.isZoomedListener, | ||||
|       required this.onSwipeDown, | ||||
|       required this.onSwipeUp, | ||||
|       this.previewUrl, | ||||
|       this.onLoadingCompleted, | ||||
|       this.onLoadingStart}) | ||||
|       : super(key: key); | ||||
|   const RemotePhotoView({ | ||||
|     Key? key, | ||||
|     required this.thumbnailUrl, | ||||
|     required this.imageUrl, | ||||
|     required this.authToken, | ||||
|     required this.isZoomedFunction, | ||||
|     required this.isZoomedListener, | ||||
|     required this.onSwipeDown, | ||||
|     required this.onSwipeUp, | ||||
|     this.previewUrl, | ||||
|     required this.onLoadingCompleted, | ||||
|     required this.onLoadingStart, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   final String thumbnailUrl; | ||||
|   final String imageUrl; | ||||
|   final String authToken; | ||||
|   final String? previewUrl; | ||||
|   final Function? onLoadingCompleted; | ||||
|   final Function? onLoadingStart; | ||||
|   final Function onLoadingCompleted; | ||||
|   final Function onLoadingStart; | ||||
|  | ||||
|   final void Function() onSwipeDown; | ||||
|   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/video_viewer_page.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'; | ||||
|  | ||||
| // ignore: must_be_immutable | ||||
| @@ -18,8 +19,6 @@ class GalleryViewerPage extends HookConsumerWidget { | ||||
|   late List<AssetResponseDto> assetList; | ||||
|   final AssetResponseDto asset; | ||||
|  | ||||
|   static const _threeStageLoading = false; | ||||
|  | ||||
|   GalleryViewerPage({ | ||||
|     Key? key, | ||||
|     required this.assetList, | ||||
| @@ -27,21 +26,35 @@ class GalleryViewerPage extends HookConsumerWidget { | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   AssetResponseDto? assetDetail; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     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); | ||||
|     final loading = useState(false); | ||||
|  | ||||
|     @override | ||||
|     void initState(int index) { | ||||
|       indexOfAsset = index; | ||||
|     } | ||||
|  | ||||
|     PageController controller = | ||||
|         PageController(initialPage: assetList.indexOf(asset)); | ||||
|  | ||||
|     useEffect( | ||||
|       () { | ||||
|         threeStageLoading.value = appSettingService | ||||
|             .getSetting<bool>(AppSettingsEnum.threeStageLoading); | ||||
|         return null; | ||||
|       }, | ||||
|       [], | ||||
|     ); | ||||
|  | ||||
|     @override | ||||
|     initState(int index) { | ||||
|       indexOfAsset = index; | ||||
|     } | ||||
|  | ||||
|     getAssetExif() async { | ||||
|       assetDetail = await ref | ||||
|           .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 | ||||
|     void isZoomedMethod() { | ||||
|       if (isZoomedListener.value) { | ||||
| @@ -84,7 +94,8 @@ class GalleryViewerPage extends HookConsumerWidget { | ||||
|           ref | ||||
|               .watch(imageViewerStateProvider.notifier) | ||||
|               .downloadAsset(assetList[indexOfAsset], context); | ||||
|         }, onSharePressed: () { | ||||
|         }, | ||||
|         onSharePressed: () { | ||||
|           ref | ||||
|               .watch(imageViewerStateProvider.notifier) | ||||
|               .shareAsset(assetList[indexOfAsset], context); | ||||
| @@ -101,17 +112,19 @@ class GalleryViewerPage extends HookConsumerWidget { | ||||
|           scrollDirection: Axis.horizontal, | ||||
|           itemBuilder: (context, index) { | ||||
|             initState(index); | ||||
|  | ||||
|             getAssetExif(); | ||||
|  | ||||
|             if (assetList[index].type == AssetTypeEnum.IMAGE) { | ||||
|               return ImageViewerPage( | ||||
|                 authToken: 'Bearer ${box.get(accessTokenKey)}', | ||||
|                 isZoomedFunction: isZoomedMethod, | ||||
|                 isZoomedListener: isZoomedListener, | ||||
|                 onLoadingCompleted: () => loading.value = false, | ||||
|                 onLoadingStart: () => loading.value = _threeStageLoading, | ||||
|                 onLoadingCompleted: () => {}, | ||||
|                 onLoadingStart: () => {}, | ||||
|                 asset: assetList[index], | ||||
|                 heroTag: assetList[index].id, | ||||
|                 threeStageLoading: _threeStageLoading | ||||
|                 threeStageLoading: threeStageLoading.value, | ||||
|               ); | ||||
|             } else { | ||||
|               return SwipeDetector( | ||||
|   | ||||
| @@ -35,6 +35,7 @@ class ImageViewerPage extends HookConsumerWidget { | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   AssetResponseDto? assetDetail; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final downloadAssetStatus = | ||||
| @@ -71,18 +72,19 @@ class ImageViewerPage extends HookConsumerWidget { | ||||
|           child: Hero( | ||||
|             tag: heroTag, | ||||
|             child: RemotePhotoView( | ||||
|                 thumbnailUrl: getThumbnailUrl(asset), | ||||
|                 imageUrl: getImageUrl(asset), | ||||
|                 previewUrl: threeStageLoading | ||||
|                     ? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG) | ||||
|                     : null, | ||||
|                 authToken: authToken, | ||||
|                 isZoomedFunction: isZoomedFunction, | ||||
|                 isZoomedListener: isZoomedListener, | ||||
|                 onSwipeDown: () => AutoRouter.of(context).pop(), | ||||
|                 onSwipeUp: () => showInfo(), | ||||
|                 onLoadingCompleted: onLoadingCompleted, | ||||
|                 onLoadingStart: onLoadingStart), | ||||
|               thumbnailUrl: getThumbnailUrl(asset), | ||||
|               imageUrl: getImageUrl(asset), | ||||
|               previewUrl: threeStageLoading | ||||
|                   ? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG) | ||||
|                   : null, | ||||
|               authToken: authToken, | ||||
|               isZoomedFunction: isZoomedFunction, | ||||
|               isZoomedListener: isZoomedListener, | ||||
|               onSwipeDown: () => AutoRouter.of(context).pop(), | ||||
|               onSwipeUp: () => showInfo(), | ||||
|               onLoadingCompleted: onLoadingCompleted, | ||||
|               onLoadingStart: onLoadingStart, | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|         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/immich_sliver_appbar.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/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_user_for_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/modules/backup/views/backup_controller_page.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart'; | ||||
| @@ -77,6 +78,7 @@ part 'router.gr.dart'; | ||||
|       guards: [AuthGuard], | ||||
|       transitionsBuilder: TransitionsBuilders.slideBottom, | ||||
|     ), | ||||
|     AutoRoute(page: SettingsPage, guards: [AuthGuard]), | ||||
|   ], | ||||
| ) | ||||
| class AppRouter extends _$AppRouter { | ||||
|   | ||||
| @@ -137,6 +137,10 @@ class _$AppRouter extends RootStackRouter { | ||||
|           opaque: true, | ||||
|           barrierDismissible: false); | ||||
|     }, | ||||
|     SettingsRoute.name: (routeData) { | ||||
|       return MaterialPageX<dynamic>( | ||||
|           routeData: routeData, child: const SettingsPage()); | ||||
|     }, | ||||
|     HomeRoute.name: (routeData) { | ||||
|       return MaterialPageX<dynamic>( | ||||
|           routeData: routeData, child: const HomePage()); | ||||
| @@ -211,7 +215,9 @@ class _$AppRouter extends RootStackRouter { | ||||
|         RouteConfig(AlbumPreviewRoute.name, | ||||
|             path: '/album-preview-page', guards: [authGuard]), | ||||
|         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'; | ||||
| } | ||||
|  | ||||
| /// 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 | ||||
| /// [HomePage] | ||||
| 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: [ | ||||
|                       BottomNavigationBarItem( | ||||
|                         label: 'tab_controller_nav_photos'.tr(), | ||||
|                         icon: const Icon(Icons.photo), | ||||
|                         icon: const Icon(Icons.photo_outlined), | ||||
|                         activeIcon: const Icon(Icons.photo), | ||||
|                       ), | ||||
|                       BottomNavigationBarItem( | ||||
|                         label: 'tab_controller_nav_search'.tr(), | ||||
|                         icon: const Icon(Icons.search), | ||||
|                         icon: const Icon(Icons.search_rounded), | ||||
|                         activeIcon: const Icon(Icons.search), | ||||
|                       ), | ||||
|                       BottomNavigationBarItem( | ||||
|                         label: 'tab_controller_nav_sharing'.tr(), | ||||
|                         icon: const Icon(Icons.group_outlined), | ||||
|                         activeIcon: const Icon(Icons.group), | ||||
|                       ), | ||||
|                       BottomNavigationBarItem( | ||||
|                         label: 'tab_controller_nav_library'.tr(), | ||||
|                         icon: const Icon( | ||||
|                           Icons.photo_album_outlined, | ||||
|                         ), | ||||
|                         icon: const Icon(Icons.photo_album_outlined), | ||||
|                         activeIcon: const Icon(Icons.photo_album_rounded), | ||||
|                       ) | ||||
|                     ], | ||||
|                   ), | ||||
|   | ||||
		Reference in New Issue
	
	Block a user