mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(mobile): Adds onboarding for permissions (#1865)
* adds onboarding * fixed error where login was taking you to permission page * fixed a bad rebase and added more checks to not start backup service on login if no gallery permission * forgot the permission handler import in AppDelegate * reverts album selection page * change to ref watch * added device_info_plus to podspec * removed unused import --------- Co-authored-by: Marty Fuhry <marty@fuhry.farm>
This commit is contained in:
		| @@ -229,5 +229,14 @@ | ||||
|   "version_announcement_overlay_text_1": "Hi friend, there is a new release of", | ||||
|   "version_announcement_overlay_text_2": "please take your time to visit the ", | ||||
|   "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", | ||||
|   "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89" | ||||
| } | ||||
|   "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", | ||||
|   "permission_onboarding_request": "Immich requires permission to view your photos and videos.", | ||||
|   "permission_onboarding_grant_permission": "Grant permission", | ||||
|   "permission_onboarding_permission_granted": "Permission granted! You are all set.", | ||||
|   "permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.", | ||||
|   "permission_onboarding_get_started": "Get started", | ||||
|   "permission_onboarding_go_to_settings": "Go to settings", | ||||
|   "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.", | ||||
|   "permission_onboarding_continue_anyway": "Continue anyway", | ||||
|   "permission_onboarding_log_out": "Log out" | ||||
| } | ||||
|   | ||||
| @@ -72,7 +72,7 @@ post_install do |installer| | ||||
|         # 'PERMISSION_SPEECH_RECOGNIZER=1', | ||||
|  | ||||
|         ## dart: PermissionGroup.photos | ||||
|         # 'PERMISSION_PHOTOS=1', | ||||
|         'PERMISSION_PHOTOS=1', | ||||
|  | ||||
|         ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse] | ||||
|         # 'PERMISSION_LOCATION=1', | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| PODS: | ||||
|   - device_info_plus (0.0.1): | ||||
|     - Flutter | ||||
|   - Flutter (1.0.0) | ||||
|   - flutter_native_splash (0.0.1): | ||||
|     - Flutter | ||||
| @@ -49,6 +51,7 @@ PODS: | ||||
|     - Flutter | ||||
|  | ||||
| DEPENDENCIES: | ||||
|   - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) | ||||
|   - Flutter (from `Flutter`) | ||||
|   - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) | ||||
|   - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) | ||||
| @@ -76,6 +79,8 @@ SPEC REPOS: | ||||
|     - Toast | ||||
|  | ||||
| EXTERNAL SOURCES: | ||||
|   device_info_plus: | ||||
|     :path: ".symlinks/plugins/device_info_plus/ios" | ||||
|   Flutter: | ||||
|     :path: Flutter | ||||
|   flutter_native_splash: | ||||
| @@ -116,6 +121,7 @@ EXTERNAL SOURCES: | ||||
|     :path: ".symlinks/plugins/wakelock/ios" | ||||
|  | ||||
| SPEC CHECKSUMS: | ||||
|   device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed | ||||
|   Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 | ||||
|   flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef | ||||
|   flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c | ||||
| @@ -139,6 +145,6 @@ SPEC CHECKSUMS: | ||||
|   video_player_avfoundation: 6d971a232d72e6ee25368378d48a079dea01f1cf | ||||
|   wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f | ||||
|  | ||||
| PODFILE CHECKSUM: 4a7e0475ea85ab7bf89955bc4c7ea9d18b54dfd8 | ||||
| PODFILE CHECKSUM: 0606648e8a9ecd5a59eafa5ab3187b45a9004a28 | ||||
|  | ||||
| COCOAPODS: 1.11.3 | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import Flutter | ||||
| import BackgroundTasks | ||||
| import path_provider_ios | ||||
| import photo_manager | ||||
| import permission_handler_apple | ||||
|  | ||||
| @UIApplicationMain | ||||
| @objc class AppDelegate: FlutterAppDelegate { | ||||
| @@ -30,6 +31,10 @@ import photo_manager | ||||
|           if !registry.hasPlugin("org.cocoapods.shared-preferences-foundation") { | ||||
|               SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.shared-preferences-foundation")!) | ||||
|           } | ||||
|  | ||||
|           if !registry.hasPlugin("org.cocoapods.permission-handler-apple") { | ||||
|               PermissionHandlerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.permission-handler-apple")!) | ||||
|           } | ||||
|       } | ||||
|        | ||||
|       return super.application(application, didFinishLaunchingWithOptions: launchOptions) | ||||
|   | ||||
| @@ -15,7 +15,8 @@ import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; | ||||
| import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart'; | ||||
| import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||
| import 'package:immich_mobile/modules/settings/providers/permission.provider.dart'; | ||||
| import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; | ||||
| import 'package:immich_mobile/modules/settings/providers/notification_permission.provider.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/routing/tab_navigation_observer.dart'; | ||||
| import 'package:immich_mobile/shared/models/immich_logger_message.model.dart'; | ||||
| @@ -34,6 +35,7 @@ import 'package:immich_mobile/utils/migration.dart'; | ||||
| import 'package:isar/isar.dart'; | ||||
| import 'package:logging/logging.dart'; | ||||
| import 'package:path_provider/path_provider.dart'; | ||||
| import 'package:permission_handler/permission_handler.dart'; | ||||
| import 'constants/hive_box.dart'; | ||||
|  | ||||
| void main() async { | ||||
| @@ -129,8 +131,10 @@ class ImmichAppState extends ConsumerState<ImmichApp> | ||||
|         ref.watch(appStateProvider.notifier).state = AppStateEnum.resumed; | ||||
|  | ||||
|         var isAuthenticated = ref.watch(authenticationProvider).isAuthenticated; | ||||
|         final permission = ref.watch(galleryPermissionNotifier); | ||||
|  | ||||
|         if (isAuthenticated) { | ||||
|         // Needs to be logged in and have gallery permissions | ||||
|         if (isAuthenticated && (permission.isGranted || permission.isLimited)) { | ||||
|           ref.read(backupProvider.notifier).resumeBackup(); | ||||
|           ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); | ||||
|           ref.watch(assetProvider.notifier).getAllAsset(); | ||||
| @@ -143,6 +147,8 @@ class ImmichAppState extends ConsumerState<ImmichApp> | ||||
|  | ||||
|         ref.watch(notificationPermissionProvider.notifier) | ||||
|           .getNotificationPermission(); | ||||
|         ref.watch(galleryPermissionNotifier.notifier) | ||||
|           .getGalleryPermissionStatus(); | ||||
|  | ||||
|         ref.read(iOSBackgroundSettingsProvider.notifier).refresh(); | ||||
|  | ||||
|   | ||||
| @@ -560,6 +560,9 @@ class BackgroundService { | ||||
|   } | ||||
|  | ||||
|   Future<DateTime?> getIOSBackupLastRun(IosBackgroundTask task) async { | ||||
|     if (!Platform.isIOS) { | ||||
|       return null; | ||||
|     } | ||||
|     // Seconds since last run | ||||
|     final double? lastRun = task == IosBackgroundTask.fetch | ||||
|         ? await _foregroundChannel.invokeMethod('lastBackgroundFetchTime') | ||||
| @@ -572,10 +575,16 @@ class BackgroundService { | ||||
|   } | ||||
|  | ||||
|   Future<int> getIOSBackupNumberOfProcesses() async { | ||||
|     if (!Platform.isIOS) { | ||||
|       return 0; | ||||
|     } | ||||
|     return await _foregroundChannel.invokeMethod('numberOfBackgroundProcesses'); | ||||
|   } | ||||
|  | ||||
|   Future<bool> getIOSBackgroundAppRefreshEnabled() async { | ||||
|     if (!Platform.isIOS) { | ||||
|       return false; | ||||
|     } | ||||
|     return await _foregroundChannel.invokeMethod('backgroundAppRefreshEnabled'); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -14,10 +14,12 @@ import 'package:immich_mobile/modules/backup/background_service/background.servi | ||||
| import 'package:immich_mobile/modules/backup/services/backup.service.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/modules/onboarding/providers/gallery_permission.provider.dart'; | ||||
| import 'package:immich_mobile/shared/providers/app_state.provider.dart'; | ||||
| import 'package:immich_mobile/shared/services/server_info.service.dart'; | ||||
| import 'package:logging/logging.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:permission_handler/permission_handler.dart'; | ||||
| import 'package:photo_manager/photo_manager.dart'; | ||||
|  | ||||
| class BackupNotifier extends StateNotifier<BackUpState> { | ||||
| @@ -26,6 +28,7 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|     this._serverInfoService, | ||||
|     this._authState, | ||||
|     this._backgroundService, | ||||
|     this._galleryPermissionNotifier, | ||||
|     this.ref, | ||||
|   ) : super( | ||||
|           BackUpState( | ||||
| @@ -65,6 +68,7 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|   final ServerInfoService _serverInfoService; | ||||
|   final AuthenticationState _authState; | ||||
|   final BackgroundService _backgroundService; | ||||
|   final GalleryPermissionNotifier _galleryPermissionNotifier; | ||||
|   final Ref ref; | ||||
|  | ||||
|   /// | ||||
| @@ -431,8 +435,8 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|  | ||||
|     await getBackupInfo(); | ||||
|  | ||||
|     var authResult = await PhotoManager.requestPermissionExtend(); | ||||
|     if (authResult.isAuth) { | ||||
|     final hasPermission = _galleryPermissionNotifier.hasPermission; | ||||
|     if (hasPermission) { | ||||
|       await PhotoManager.clearFileCache(); | ||||
|  | ||||
|       if (state.allUniqueAssets.isEmpty) { | ||||
| @@ -463,7 +467,7 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|       ); | ||||
|       await _notifyBackgroundServiceCanRun(); | ||||
|     } else { | ||||
|       PhotoManager.openSetting(); | ||||
|       openAppSettings(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -704,6 +708,7 @@ final backupProvider = | ||||
|     ref.watch(serverInfoServiceProvider), | ||||
|     ref.watch(authenticationProvider), | ||||
|     ref.watch(backgroundServiceProvider), | ||||
|     ref.watch(galleryPermissionNotifier.notifier), | ||||
|     ref, | ||||
|   ); | ||||
| }); | ||||
|   | ||||
| @@ -7,14 +7,18 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/oauth.provider.dart'; | ||||
| import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/shared/providers/api.provider.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/ui/immich_logo.dart'; | ||||
| import 'package:immich_mobile/shared/ui/immich_title_text.dart'; | ||||
| import 'package:immich_mobile/shared/ui/immich_toast.dart'; | ||||
| import 'package:immich_mobile/utils/url_helper.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:permission_handler/permission_handler.dart'; | ||||
|  | ||||
| class LoginForm extends HookConsumerWidget { | ||||
|   const LoginForm({Key? key}) : super(key: key); | ||||
| @@ -105,22 +109,12 @@ class LoginForm extends HookConsumerWidget { | ||||
|                   onDoubleTap: () => populateTestLoginInfo(), | ||||
|                   child: RotationTransition( | ||||
|                     turns: logoAnimationController, | ||||
|                     child: const Image( | ||||
|                       image: AssetImage('assets/immich-logo-no-outline.png'), | ||||
|                       width: 100, | ||||
|                       filterQuality: FilterQuality.high, | ||||
|                     child: const ImmichLogo( | ||||
|                       heroTag: 'logo', | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|                 Text( | ||||
|                   'IMMICH', | ||||
|                   style: TextStyle( | ||||
|                     fontFamily: 'SnowburstOne', | ||||
|                     fontWeight: FontWeight.bold, | ||||
|                     fontSize: 48, | ||||
|                     color: Theme.of(context).primaryColor, | ||||
|                   ), | ||||
|                 ), | ||||
|                 const ImmichTitleText(), | ||||
|                 EmailInput(controller: usernameController), | ||||
|                 PasswordInput(controller: passwordController), | ||||
|                 ServerEndpointInput( | ||||
| @@ -164,7 +158,10 @@ class LoginForm extends HookConsumerWidget { | ||||
|                           isLoading: isLoading, | ||||
|                           onLoginSuccess: () { | ||||
|                             isLoading.value = false; | ||||
|                             ref.watch(backupProvider.notifier).resumeBackup(); | ||||
|                             final permission = ref.watch(galleryPermissionNotifier); | ||||
|                             if (permission.isGranted || permission.isLimited) { | ||||
|                               ref.watch(backupProvider.notifier).resumeBackup(); | ||||
|                             } | ||||
|                             AutoRouter.of(context).replace( | ||||
|                               const TabControllerRoute(), | ||||
|                             ); | ||||
| @@ -313,7 +310,13 @@ class LoginButton extends ConsumerWidget { | ||||
|               !ref.read(authenticationProvider).isAdmin) { | ||||
|             AutoRouter.of(context).push(const ChangePasswordRoute()); | ||||
|           } else { | ||||
|             ref.read(backupProvider.notifier).resumeBackup(); | ||||
|             final hasPermission = await ref | ||||
|                 .read(galleryPermissionNotifier.notifier) | ||||
|                 .hasPermission; | ||||
|             if (hasPermission) { | ||||
|               // Don't resume the backup until we have gallery permission | ||||
|               ref.read(backupProvider.notifier).resumeBackup(); | ||||
|             } | ||||
|             AutoRouter.of(context).replace(const TabControllerRoute()); | ||||
|           } | ||||
|         } else { | ||||
|   | ||||
| @@ -0,0 +1,101 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:device_info_plus/device_info_plus.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:permission_handler/permission_handler.dart'; | ||||
|  | ||||
| class GalleryPermissionNotifier extends StateNotifier<PermissionStatus> { | ||||
|   GalleryPermissionNotifier() | ||||
|     : super(PermissionStatus.denied)  // Denied is the intitial state | ||||
|   { | ||||
|     // Sets the initial state | ||||
|     getGalleryPermissionStatus(); | ||||
|   } | ||||
|  | ||||
|   get hasPermission => state.isGranted || state.isLimited; | ||||
|  | ||||
|   /// Requests the gallery permission | ||||
|   Future<PermissionStatus> requestGalleryPermission() async { | ||||
|     // Android 32 and below uses Permission.storage | ||||
|     if (Platform.isAndroid) { | ||||
|       final androidInfo = await DeviceInfoPlugin().androidInfo; | ||||
|       if (androidInfo.version.sdkInt <= 32) { | ||||
|         // Android 32 and below need storage | ||||
|         final permission = await Permission.storage.request(); | ||||
|         state = permission; | ||||
|         return permission; | ||||
|       } else { | ||||
|         // Android 33 need photo & video | ||||
|         final photos = await Permission.photos.request(); | ||||
|         if (!photos.isGranted) { | ||||
|           // Don't ask twice for the same permission | ||||
|           return photos; | ||||
|         } | ||||
|         final videos = await Permission.videos.request(); | ||||
|  | ||||
|         // Return the joint result of those two permissions | ||||
|         final PermissionStatus status; | ||||
|         if (photos.isGranted && videos.isGranted) { | ||||
|           status = PermissionStatus.granted; | ||||
|         } else if (photos.isDenied || videos.isDenied) { | ||||
|           status = PermissionStatus.denied; | ||||
|         } else if (photos.isPermanentlyDenied || videos.isPermanentlyDenied) { | ||||
|           status = PermissionStatus.permanentlyDenied; | ||||
|         } else { | ||||
|           status = PermissionStatus.denied; | ||||
|         } | ||||
|  | ||||
|         state = status; | ||||
|         return status; | ||||
|       } | ||||
|     } else { | ||||
|       // iOS can use photos | ||||
|       final photos = await Permission.photos.request(); | ||||
|       state = photos; | ||||
|       return photos; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Checks the current state of the gallery permissions without | ||||
|   /// requesting them again | ||||
|   Future<PermissionStatus> getGalleryPermissionStatus() async { | ||||
|     // Android 32 and below uses Permission.storage | ||||
|     if (Platform.isAndroid) { | ||||
|       final androidInfo = await DeviceInfoPlugin().androidInfo; | ||||
|       if (androidInfo.version.sdkInt <= 32) { | ||||
|         // Android 32 and below need storage | ||||
|         final permission = await Permission.storage.status; | ||||
|         state = permission; | ||||
|         return permission; | ||||
|       } else { | ||||
|         // Android 33 needs photo & video | ||||
|         final photos = await Permission.photos.status; | ||||
|         final videos = await Permission.videos.status; | ||||
|  | ||||
|         // Return the joint result of those two permissions | ||||
|         final PermissionStatus status; | ||||
|         if (photos.isGranted && videos.isGranted) { | ||||
|           status = PermissionStatus.granted; | ||||
|         } else if (photos.isDenied || videos.isDenied) { | ||||
|           status = PermissionStatus.denied; | ||||
|         } else if (photos.isPermanentlyDenied || videos.isPermanentlyDenied) { | ||||
|           status = PermissionStatus.permanentlyDenied; | ||||
|         } else { | ||||
|           status = PermissionStatus.denied; | ||||
|         } | ||||
|  | ||||
|         state = status; | ||||
|         return status; | ||||
|       } | ||||
|     } else { | ||||
|       // iOS can use photos | ||||
|       final photos = await Permission.photos.status; | ||||
|       state = photos; | ||||
|       return photos; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| final galleryPermissionNotifier | ||||
|   = StateNotifierProvider<GalleryPermissionNotifier, PermissionStatus> | ||||
|     ((ref) => GalleryPermissionNotifier()); | ||||
| @@ -0,0 +1,201 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||
| import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/shared/ui/immich_logo.dart'; | ||||
| import 'package:immich_mobile/shared/ui/immich_title_text.dart'; | ||||
| import 'package:permission_handler/permission_handler.dart'; | ||||
|  | ||||
| class PermissionOnboardingPage extends HookConsumerWidget { | ||||
|  | ||||
|   const PermissionOnboardingPage({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final PermissionStatus permission = ref.watch(galleryPermissionNotifier); | ||||
|  | ||||
|     // Navigate to the main Tab Controller when permission is granted | ||||
|     void goToHome() { | ||||
|       // Resume backup (if enable) then navigate | ||||
|       ref.watch(backupProvider.notifier).resumeBackup() | ||||
|         .catchError((error) { | ||||
|         debugPrint('PermissionOnboardingPage error: $error'); | ||||
|       }); | ||||
|       AutoRouter.of(context).replace( | ||||
|         const TabControllerRoute(), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     // When the permission is denied, we show a request permission page | ||||
|     buildRequestPermission() { | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.center, | ||||
|         mainAxisAlignment: MainAxisAlignment.center, | ||||
|         children: [ | ||||
|           Text( | ||||
|             'permission_onboarding_request', | ||||
|             style: Theme.of(context).textTheme.titleMedium, | ||||
|             textAlign: TextAlign.center, | ||||
|           ).tr(), | ||||
|           const SizedBox(height: 18), | ||||
|           ElevatedButton( | ||||
|             onPressed: () => ref | ||||
|               .read(galleryPermissionNotifier.notifier) | ||||
|               .requestGalleryPermission() | ||||
|               .then((permission) async { | ||||
|                 if (permission.isGranted) { | ||||
|                   // If permission is limited, we will show the limited | ||||
|                   // permission page | ||||
|                   goToHome(); | ||||
|                 } | ||||
|               }), | ||||
|             child: const Text( | ||||
|               'permission_onboarding_grant_permission', | ||||
|             ).tr(), | ||||
|           ), | ||||
|         ], | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     // When permission is granted from outside the app, this will show to | ||||
|     // let them continue on to the main timeline | ||||
|     buildPermissionGranted() { | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.center, | ||||
|         mainAxisAlignment: MainAxisAlignment.center, | ||||
|         children: [ | ||||
|           Text( | ||||
|             'permission_onboarding_permission_granted', | ||||
|             style: Theme.of(context).textTheme.titleMedium, | ||||
|             textAlign: TextAlign.center, | ||||
|           ).tr(), | ||||
|           const SizedBox(height: 18), | ||||
|           ElevatedButton( | ||||
|             onPressed: () => goToHome(), | ||||
|             child: const Text('permission_onboarding_get_started').tr(), | ||||
|           ), | ||||
|         ], | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     // iOS 14+ has limited permission options, which let someone just share | ||||
|     // a few photos with the app. If someone only has limited permissions, we | ||||
|     // inform that Immich works best when given full permission | ||||
|     buildPermissionLimited() { | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.center, | ||||
|         mainAxisAlignment: MainAxisAlignment.center, | ||||
|         children: [ | ||||
|           const Icon(Icons.warning_outlined, | ||||
|             color: Colors.yellow, | ||||
|             size: 48, | ||||
|           ), | ||||
|           const SizedBox(height: 8), | ||||
|           Text( | ||||
|             'permission_onboarding_permission_limited', | ||||
|             style: Theme.of(context).textTheme.titleMedium, | ||||
|             textAlign: TextAlign.center, | ||||
|           ).tr(), | ||||
|           const SizedBox(height: 18), | ||||
|           ElevatedButton( | ||||
|             onPressed: () => openAppSettings(), | ||||
|             child: const Text( | ||||
|               'permission_onboarding_go_to_settings', | ||||
|             ).tr(), | ||||
|           ), | ||||
|           const SizedBox(height: 8.0), | ||||
|           TextButton( | ||||
|             onPressed: () => goToHome(), | ||||
|             child: const Text( | ||||
|               'permission_onboarding_continue_anyway', | ||||
|             ).tr(), | ||||
|           ), | ||||
|         ], | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     buildPermissionDenied() { | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.center, | ||||
|         mainAxisAlignment: MainAxisAlignment.center, | ||||
|         children: [ | ||||
|           const Icon(Icons.warning_outlined, | ||||
|             color: Colors.red, | ||||
|             size: 48, | ||||
|           ), | ||||
|           const SizedBox(height: 8), | ||||
|           Text( | ||||
|             'permission_onboarding_permission_denied', | ||||
|             style: Theme.of(context).textTheme.titleMedium, | ||||
|             textAlign: TextAlign.center, | ||||
|           ).tr(), | ||||
|           const SizedBox(height: 18), | ||||
|           ElevatedButton( | ||||
|             onPressed: () => openAppSettings(), | ||||
|             child: const Text( | ||||
|               'permission_onboarding_go_to_settings', | ||||
|             ).tr(), | ||||
|           ), | ||||
|         ], | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     final Widget child; | ||||
|     switch (permission) { | ||||
|       case PermissionStatus.limited: | ||||
|         child = buildPermissionLimited(); | ||||
|         break; | ||||
|       case PermissionStatus.denied: | ||||
|         child = buildRequestPermission(); | ||||
|         break; | ||||
|       case PermissionStatus.granted: | ||||
|         child = buildPermissionGranted(); | ||||
|         break; | ||||
|       case PermissionStatus.restricted: | ||||
|       case PermissionStatus.permanentlyDenied: | ||||
|         child = buildPermissionDenied(); | ||||
|         break; | ||||
|     } | ||||
|  | ||||
|     return Scaffold( | ||||
|       body: SafeArea( | ||||
|         child: Center( | ||||
|           child: SizedBox( | ||||
|             width: 380, | ||||
|             child: Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.center, | ||||
|               mainAxisAlignment: MainAxisAlignment.center, | ||||
|               children: [ | ||||
|                 const ImmichLogo( | ||||
|                   heroTag: 'logo', | ||||
|                 ), | ||||
|                 const ImmichTitleText(), | ||||
|                 AnimatedSwitcher( | ||||
|                   duration: const Duration(milliseconds: 500), | ||||
|                   child: Padding( | ||||
|                     padding: const EdgeInsets.all(18.0), | ||||
|                     child: child, | ||||
|                   ), | ||||
|                 ), | ||||
|                 TextButton( | ||||
|                   child: const Text('permission_onboarding_log_out').tr(), | ||||
|                   onPressed: () {  | ||||
|                     ref.read(authenticationProvider.notifier).logout(); | ||||
|                     AutoRouter.of(context).replace( | ||||
|                       const LoginRoute(), | ||||
|                     ); | ||||
|                   }, | ||||
|                 ), | ||||
|               ], | ||||
|  | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,21 +0,0 @@ | ||||
| import 'package:permission_handler/permission_handler.dart'; | ||||
|  | ||||
| /// This class is for requesting permissions in the app | ||||
| class PermissionService { | ||||
|   /// Requests the notification permission | ||||
|   /// Note: In Android, this is always granted | ||||
|   Future<PermissionStatus> requestNotificationPermission() { | ||||
|     return Permission.notification.request(); | ||||
|   } | ||||
|  | ||||
|   /// Whether the user has the permission or not | ||||
|   /// Note: In Android, this is always true | ||||
|   Future<bool> hasNotificationPermission() { | ||||
|     return Permission.notification.isGranted; | ||||
|   } | ||||
|  | ||||
|   /// Either the permission was granted already or else ask for the permission | ||||
|   Future<bool> hasOrAskForNotificationPermission() { | ||||
|     return requestNotificationPermission().then((p) => p.isGranted); | ||||
|   } | ||||
| } | ||||
| @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; | ||||
| import 'package:immich_mobile/modules/settings/providers/permission.provider.dart'; | ||||
| import 'package:immich_mobile/modules/settings/providers/notification_permission.provider.dart'; | ||||
| import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; | ||||
| import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart'; | ||||
| import 'package:permission_handler/permission_handler.dart'; | ||||
|   | ||||
							
								
								
									
										19
									
								
								mobile/lib/routing/gallery_permission_guard.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								mobile/lib/routing/gallery_permission_guard.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
|  | ||||
| class GalleryPermissionGuard extends AutoRouteGuard { | ||||
|   final GalleryPermissionNotifier _permission; | ||||
|  | ||||
|   GalleryPermissionGuard(this._permission); | ||||
|  | ||||
|   @override | ||||
|   void onNavigation(NavigationResolver resolver, StackRouter router) async { | ||||
|     final p = _permission.hasPermission; | ||||
|     if (p) { | ||||
|       resolver.next(true); | ||||
|     } else { | ||||
|       router.replaceAll([const PermissionOnboardingRoute()]); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -19,11 +19,14 @@ import 'package:immich_mobile/modules/favorite/views/favorites_page.dart'; | ||||
| import 'package:immich_mobile/modules/home/views/home_page.dart'; | ||||
| import 'package:immich_mobile/modules/login/views/change_password_page.dart'; | ||||
| import 'package:immich_mobile/modules/login/views/login_page.dart'; | ||||
| import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; | ||||
| import 'package:immich_mobile/modules/onboarding/views/permission_onboarding_page.dart'; | ||||
| import 'package:immich_mobile/modules/search/views/search_page.dart'; | ||||
| import 'package:immich_mobile/modules/search/views/search_result_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/duplicate_guard.dart'; | ||||
| import 'package:immich_mobile/routing/gallery_permission_guard.dart'; | ||||
| import 'package:immich_mobile/shared/models/asset.dart'; | ||||
| import 'package:immich_mobile/shared/models/album.dart'; | ||||
| import 'package:immich_mobile/shared/providers/api.provider.dart'; | ||||
| @@ -39,6 +42,7 @@ part 'router.gr.dart'; | ||||
|   replaceInRouteName: 'Page,Route', | ||||
|   routes: <AutoRoute>[ | ||||
|     AutoRoute(page: SplashScreenPage, initial: true), | ||||
|     AutoRoute(page: PermissionOnboardingPage, guards: [AuthGuard, DuplicateGuard]), | ||||
|     AutoRoute(page: LoginPage, | ||||
|       guards: [ | ||||
|         DuplicateGuard, | ||||
| @@ -47,7 +51,7 @@ part 'router.gr.dart'; | ||||
|     AutoRoute(page: ChangePasswordPage), | ||||
|     CustomRoute( | ||||
|       page: TabControllerPage, | ||||
|       guards: [AuthGuard, DuplicateGuard], | ||||
|       guards: [AuthGuard, DuplicateGuard, GalleryPermissionGuard], | ||||
|       children: [ | ||||
|         AutoRoute(page: HomePage, guards: [AuthGuard, DuplicateGuard]), | ||||
|         AutoRoute(page: SearchPage, guards: [AuthGuard, DuplicateGuard]), | ||||
| @@ -56,7 +60,7 @@ part 'router.gr.dart'; | ||||
|       ], | ||||
|       transitionsBuilder: TransitionsBuilders.fadeIn, | ||||
|     ), | ||||
|     AutoRoute(page: GalleryViewerPage, guards: [AuthGuard, DuplicateGuard]), | ||||
|     AutoRoute(page: GalleryViewerPage, guards: [AuthGuard, DuplicateGuard, GalleryPermissionGuard]), | ||||
|     AutoRoute(page: VideoViewerPage, guards: [AuthGuard, DuplicateGuard]), | ||||
|     AutoRoute(page: BackupControllerPage, guards: [AuthGuard, DuplicateGuard]), | ||||
|     AutoRoute(page: SearchResultPage, guards: [AuthGuard, DuplicateGuard]), | ||||
| @@ -101,12 +105,15 @@ class AppRouter extends _$AppRouter { | ||||
|   // ignore: unused_field | ||||
|   final ApiService _apiService; | ||||
|  | ||||
|   AppRouter(this._apiService)  | ||||
|       : super( | ||||
|   AppRouter( | ||||
|     this._apiService,  | ||||
|     GalleryPermissionNotifier galleryPermissionNotifier, | ||||
|     ) : super( | ||||
|           authGuard: AuthGuard(_apiService),  | ||||
|           duplicateGuard: DuplicateGuard(), | ||||
|           galleryPermissionGuard: GalleryPermissionGuard(galleryPermissionNotifier), | ||||
|         ); | ||||
| } | ||||
|  | ||||
| final appRouterProvider = | ||||
|     Provider((ref) => AppRouter(ref.watch(apiServiceProvider))); | ||||
|     Provider((ref) => AppRouter(ref.watch(apiServiceProvider), ref.watch(galleryPermissionNotifier.notifier))); | ||||
|   | ||||
| @@ -15,13 +15,16 @@ part of 'router.dart'; | ||||
| class _$AppRouter extends RootStackRouter { | ||||
|   _$AppRouter({ | ||||
|     GlobalKey<NavigatorState>? navigatorKey, | ||||
|     required this.duplicateGuard, | ||||
|     required this.authGuard, | ||||
|     required this.duplicateGuard, | ||||
|     required this.galleryPermissionGuard, | ||||
|   }) : super(navigatorKey); | ||||
|  | ||||
|   final AuthGuard authGuard; | ||||
|  | ||||
|   final DuplicateGuard duplicateGuard; | ||||
|  | ||||
|   final AuthGuard authGuard; | ||||
|   final GalleryPermissionGuard galleryPermissionGuard; | ||||
|  | ||||
|   @override | ||||
|   final Map<String, PageFactory> pagesMap = { | ||||
| @@ -31,6 +34,12 @@ class _$AppRouter extends RootStackRouter { | ||||
|         child: const SplashScreenPage(), | ||||
|       ); | ||||
|     }, | ||||
|     PermissionOnboardingRoute.name: (routeData) { | ||||
|       return MaterialPageX<dynamic>( | ||||
|         routeData: routeData, | ||||
|         child: const PermissionOnboardingPage(), | ||||
|       ); | ||||
|     }, | ||||
|     LoginRoute.name: (routeData) { | ||||
|       return MaterialPageX<dynamic>( | ||||
|         routeData: routeData, | ||||
| @@ -225,6 +234,14 @@ class _$AppRouter extends RootStackRouter { | ||||
|           SplashScreenRoute.name, | ||||
|           path: '/', | ||||
|         ), | ||||
|         RouteConfig( | ||||
|           PermissionOnboardingRoute.name, | ||||
|           path: '/permission-onboarding-page', | ||||
|           guards: [ | ||||
|             authGuard, | ||||
|             duplicateGuard, | ||||
|           ], | ||||
|         ), | ||||
|         RouteConfig( | ||||
|           LoginRoute.name, | ||||
|           path: '/login-page', | ||||
| @@ -240,6 +257,7 @@ class _$AppRouter extends RootStackRouter { | ||||
|           guards: [ | ||||
|             authGuard, | ||||
|             duplicateGuard, | ||||
|             galleryPermissionGuard, | ||||
|           ], | ||||
|           children: [ | ||||
|             RouteConfig( | ||||
| @@ -286,6 +304,7 @@ class _$AppRouter extends RootStackRouter { | ||||
|           guards: [ | ||||
|             authGuard, | ||||
|             duplicateGuard, | ||||
|             galleryPermissionGuard, | ||||
|           ], | ||||
|         ), | ||||
|         RouteConfig( | ||||
| @@ -411,6 +430,18 @@ class SplashScreenRoute extends PageRouteInfo<void> { | ||||
|   static const String name = 'SplashScreenRoute'; | ||||
| } | ||||
|  | ||||
| /// generated route for | ||||
| /// [PermissionOnboardingPage] | ||||
| class PermissionOnboardingRoute extends PageRouteInfo<void> { | ||||
|   const PermissionOnboardingRoute() | ||||
|       : super( | ||||
|           PermissionOnboardingRoute.name, | ||||
|           path: '/permission-onboarding-page', | ||||
|         ); | ||||
|  | ||||
|   static const String name = 'PermissionOnboardingRoute'; | ||||
| } | ||||
|  | ||||
| /// generated route for | ||||
| /// [LoginPage] | ||||
| class LoginRoute extends PageRouteInfo<void> { | ||||
|   | ||||
							
								
								
									
										25
									
								
								mobile/lib/shared/ui/immich_logo.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								mobile/lib/shared/ui/immich_logo.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
| class ImmichLogo extends StatelessWidget { | ||||
|   final double size; | ||||
|   final dynamic heroTag; | ||||
|  | ||||
|   const ImmichLogo({ | ||||
|     super.key, | ||||
|     this.size = 100, | ||||
|     this.heroTag, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Hero( | ||||
|       tag: heroTag, | ||||
|       child: Image( | ||||
|         image: const AssetImage('assets/immich-logo-no-outline.png'), | ||||
|         width: size, | ||||
|         filterQuality: FilterQuality.high, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										26
									
								
								mobile/lib/shared/ui/immich_title_text.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								mobile/lib/shared/ui/immich_title_text.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
| class ImmichTitleText extends StatelessWidget { | ||||
|   final double fontSize; | ||||
|   final Color? color; | ||||
|  | ||||
|   const ImmichTitleText({ | ||||
|     super.key, | ||||
|     this.fontSize = 48, | ||||
|     this.color, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Text( | ||||
|       'IMMICH', | ||||
|       style: TextStyle( | ||||
|         fontFamily: 'SnowburstOne', | ||||
|         fontWeight: FontWeight.bold, | ||||
|         fontSize: fontSize, | ||||
|         color: color ?? Theme.of(context).primaryColor, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -7,6 +7,7 @@ import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; | ||||
| import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||
| import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/shared/providers/api.provider.dart'; | ||||
|  | ||||
| @@ -32,8 +33,13 @@ class SplashScreenPage extends HookConsumerWidget { | ||||
|                 serverUrl: loginInfo.serverUrl, | ||||
|               ); | ||||
|           if (isSuccess) { | ||||
|             // Resume backup (if enable) then navigate | ||||
|             ref.watch(backupProvider.notifier).resumeBackup(); | ||||
|             final hasPermission = await ref | ||||
|                 .read(galleryPermissionNotifier.notifier) | ||||
|                 .hasPermission; | ||||
|             if (hasPermission) { | ||||
|               // Resume backup (if enable) then navigate | ||||
|               ref.watch(backupProvider.notifier).resumeBackup(); | ||||
|             } | ||||
|             AutoRouter.of(context).replace(const TabControllerRoute()); | ||||
|           } else { | ||||
|             AutoRouter.of(context).replace(const LoginRoute()); | ||||
|   | ||||
| @@ -281,6 +281,22 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.1.0" | ||||
|   device_info_plus: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: device_info_plus | ||||
|       sha256: "1d6e5a61674ba3a68fb048a7c7b4ff4bebfed8d7379dbe8f2b718231be9a7c95" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "8.1.0" | ||||
|   device_info_plus_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: device_info_plus_platform_interface | ||||
|       sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "7.0.0" | ||||
|   easy_image_viewer: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|   | ||||
| @@ -46,6 +46,7 @@ dependencies: | ||||
|   isar: *isar_version | ||||
|   isar_flutter_libs: *isar_version # contains Isar Core | ||||
|   permission_handler: ^10.2.0 | ||||
|   device_info_plus: ^8.1.0 | ||||
|  | ||||
|   openapi: | ||||
|     path: openapi | ||||
|   | ||||
		Reference in New Issue
	
	Block a user