1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-04 02:46:47 +01:00

Merge branch 'main' of github.com:immich-app/immich

This commit is contained in:
Alex Tran 2023-02-28 21:21:40 -06:00
commit 5777693fad
No known key found for this signature in database
GPG key ID: E4954BC787B85C8A
24 changed files with 952 additions and 276 deletions

View file

@ -229,5 +229,14 @@
"version_announcement_overlay_text_1": "Hi friend, there is a new release of", "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_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_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"
}

View file

@ -72,7 +72,7 @@ post_install do |installer|
# 'PERMISSION_SPEECH_RECOGNIZER=1', # 'PERMISSION_SPEECH_RECOGNIZER=1',
## dart: PermissionGroup.photos ## dart: PermissionGroup.photos
# 'PERMISSION_PHOTOS=1', 'PERMISSION_PHOTOS=1',
## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse] ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
# 'PERMISSION_LOCATION=1', # 'PERMISSION_LOCATION=1',

View file

@ -1,4 +1,6 @@
PODS: PODS:
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_native_splash (0.0.1): - flutter_native_splash (0.0.1):
- Flutter - Flutter
@ -49,6 +51,7 @@ PODS:
- Flutter - Flutter
DEPENDENCIES: DEPENDENCIES:
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
@ -76,6 +79,8 @@ SPEC REPOS:
- Toast - Toast
EXTERNAL SOURCES: EXTERNAL SOURCES:
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_native_splash: flutter_native_splash:
@ -116,6 +121,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock/ios" :path: ".symlinks/plugins/wakelock/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c
@ -139,6 +145,6 @@ SPEC CHECKSUMS:
video_player_avfoundation: 6d971a232d72e6ee25368378d48a079dea01f1cf video_player_avfoundation: 6d971a232d72e6ee25368378d48a079dea01f1cf
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
PODFILE CHECKSUM: 4a7e0475ea85ab7bf89955bc4c7ea9d18b54dfd8 PODFILE CHECKSUM: 0606648e8a9ecd5a59eafa5ab3187b45a9004a28
COCOAPODS: 1.11.3 COCOAPODS: 1.11.3

View file

@ -4,6 +4,7 @@ import Flutter
import BackgroundTasks import BackgroundTasks
import path_provider_ios import path_provider_ios
import photo_manager import photo_manager
import permission_handler_apple
@UIApplicationMain @UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate {
@ -30,6 +31,10 @@ import photo_manager
if !registry.hasPlugin("org.cocoapods.shared-preferences-foundation") { if !registry.hasPlugin("org.cocoapods.shared-preferences-foundation") {
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "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) return super.application(application, didFinishLaunchingWithOptions: launchOptions)

View file

@ -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/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/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.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/router.dart';
import 'package:immich_mobile/routing/tab_navigation_observer.dart'; import 'package:immich_mobile/routing/tab_navigation_observer.dart';
import 'package:immich_mobile/shared/models/immich_logger_message.model.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:isar/isar.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'constants/hive_box.dart'; import 'constants/hive_box.dart';
void main() async { void main() async {
@ -129,8 +131,10 @@ class ImmichAppState extends ConsumerState<ImmichApp>
ref.watch(appStateProvider.notifier).state = AppStateEnum.resumed; ref.watch(appStateProvider.notifier).state = AppStateEnum.resumed;
var isAuthenticated = ref.watch(authenticationProvider).isAuthenticated; 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(backupProvider.notifier).resumeBackup();
ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
ref.watch(assetProvider.notifier).getAllAsset(); ref.watch(assetProvider.notifier).getAllAsset();
@ -143,6 +147,8 @@ class ImmichAppState extends ConsumerState<ImmichApp>
ref.watch(notificationPermissionProvider.notifier) ref.watch(notificationPermissionProvider.notifier)
.getNotificationPermission(); .getNotificationPermission();
ref.watch(galleryPermissionNotifier.notifier)
.getGalleryPermissionStatus();
ref.read(iOSBackgroundSettingsProvider.notifier).refresh(); ref.read(iOSBackgroundSettingsProvider.notifier).refresh();

View file

@ -560,6 +560,9 @@ class BackgroundService {
} }
Future<DateTime?> getIOSBackupLastRun(IosBackgroundTask task) async { Future<DateTime?> getIOSBackupLastRun(IosBackgroundTask task) async {
if (!Platform.isIOS) {
return null;
}
// Seconds since last run // Seconds since last run
final double? lastRun = task == IosBackgroundTask.fetch final double? lastRun = task == IosBackgroundTask.fetch
? await _foregroundChannel.invokeMethod('lastBackgroundFetchTime') ? await _foregroundChannel.invokeMethod('lastBackgroundFetchTime')
@ -572,10 +575,16 @@ class BackgroundService {
} }
Future<int> getIOSBackupNumberOfProcesses() async { Future<int> getIOSBackupNumberOfProcesses() async {
if (!Platform.isIOS) {
return 0;
}
return await _foregroundChannel.invokeMethod('numberOfBackgroundProcesses'); return await _foregroundChannel.invokeMethod('numberOfBackgroundProcesses');
} }
Future<bool> getIOSBackgroundAppRefreshEnabled() async { Future<bool> getIOSBackgroundAppRefreshEnabled() async {
if (!Platform.isIOS) {
return false;
}
return await _foregroundChannel.invokeMethod('backgroundAppRefreshEnabled'); return await _foregroundChannel.invokeMethod('backgroundAppRefreshEnabled');
} }
} }

View file

@ -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/backup/services/backup.service.dart';
import 'package:immich_mobile/modules/login/models/authentication_state.model.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/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/providers/app_state.provider.dart';
import 'package:immich_mobile/shared/services/server_info.service.dart'; import 'package:immich_mobile/shared/services/server_info.service.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
class BackupNotifier extends StateNotifier<BackUpState> { class BackupNotifier extends StateNotifier<BackUpState> {
@ -26,6 +28,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
this._serverInfoService, this._serverInfoService,
this._authState, this._authState,
this._backgroundService, this._backgroundService,
this._galleryPermissionNotifier,
this.ref, this.ref,
) : super( ) : super(
BackUpState( BackUpState(
@ -65,6 +68,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
final ServerInfoService _serverInfoService; final ServerInfoService _serverInfoService;
final AuthenticationState _authState; final AuthenticationState _authState;
final BackgroundService _backgroundService; final BackgroundService _backgroundService;
final GalleryPermissionNotifier _galleryPermissionNotifier;
final Ref ref; final Ref ref;
/// ///
@ -431,8 +435,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
await getBackupInfo(); await getBackupInfo();
var authResult = await PhotoManager.requestPermissionExtend(); final hasPermission = _galleryPermissionNotifier.hasPermission;
if (authResult.isAuth) { if (hasPermission) {
await PhotoManager.clearFileCache(); await PhotoManager.clearFileCache();
if (state.allUniqueAssets.isEmpty) { if (state.allUniqueAssets.isEmpty) {
@ -463,7 +467,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
); );
await _notifyBackgroundServiceCanRun(); await _notifyBackgroundServiceCanRun();
} else { } else {
PhotoManager.openSetting(); openAppSettings();
} }
} }
@ -704,6 +708,7 @@ final backupProvider =
ref.watch(serverInfoServiceProvider), ref.watch(serverInfoServiceProvider),
ref.watch(authenticationProvider), ref.watch(authenticationProvider),
ref.watch(backgroundServiceProvider), ref.watch(backgroundServiceProvider),
ref.watch(galleryPermissionNotifier.notifier),
ref, ref,
); );
}); });

View file

@ -137,6 +137,7 @@ class AlbumInfoCard extends HookConsumerWidget {
} }
}, },
child: Card( child: Card(
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.all(1), margin: const EdgeInsets.all(1),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), // if you need this borderRadius: BorderRadius.circular(12), // if you need this
@ -150,20 +151,17 @@ class AlbumInfoCard extends HookConsumerWidget {
elevation: 0, elevation: 0,
borderOnForeground: false, borderOnForeground: false,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Stack( Expanded(
children: [ child: Stack(
Container( clipBehavior: Clip.hardEdge,
width: 200, children: [
height: 200, ColorFiltered(
decoration: BoxDecoration( colorFilter: buildImageFilter(),
borderRadius: const BorderRadius.only( child: Image(
topLeft: Radius.circular(12), width: double.infinity,
topRight: Radius.circular(12), height: double.infinity,
),
image: DecorationImage(
colorFilter: buildImageFilter(),
image: imageData != null image: imageData != null
? MemoryImage(imageData!) ? MemoryImage(imageData!)
: const AssetImage( : const AssetImage(
@ -172,58 +170,56 @@ class AlbumInfoCard extends HookConsumerWidget {
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
child: null, Positioned(
), bottom: 10,
Positioned( right: 25,
bottom: 10, child: buildSelectedTextBox(),
left: 25, )
child: buildSelectedTextBox(), ],
) ),
],
), ),
Padding( Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(
left: 25,
),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
SizedBox( Expanded(
width: 140, child: Column(
child: Padding( crossAxisAlignment: CrossAxisAlignment.start,
padding: const EdgeInsets.only(left: 25.0), mainAxisAlignment: MainAxisAlignment.center,
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, Text(
children: [ albumInfo.name,
Text( style: TextStyle(
albumInfo.name, fontSize: 14,
style: TextStyle( color: Theme.of(context).primaryColor,
fontSize: 14, fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
), ),
Padding( ),
padding: const EdgeInsets.only(top: 2.0), Padding(
child: FutureBuilder( padding: const EdgeInsets.only(top: 2.0),
builder: ((context, snapshot) { child: FutureBuilder(
if (snapshot.hasData) { builder: ((context, snapshot) {
return Text( if (snapshot.hasData) {
snapshot.data.toString() + return Text(
(albumInfo.isAll snapshot.data.toString() +
? " (${'backup_all'.tr()})" (albumInfo.isAll
: ""), ? " (${'backup_all'.tr()})"
style: TextStyle( : ""),
fontSize: 12, style: TextStyle(
color: Colors.grey[600], fontSize: 12,
), color: Colors.grey[600],
); ),
} );
return const Text("0"); }
}), return const Text("0");
future: albumInfo.assetCount, }),
), future: albumInfo.assetCount,
) ),
], )
), ],
), ),
), ),
IconButton( IconButton(

View file

@ -0,0 +1,176 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
class AlbumInfoListTile extends HookConsumerWidget {
final Uint8List? imageData;
final AvailableAlbum albumInfo;
const AlbumInfoListTile({Key? key, this.imageData, required this.albumInfo})
: super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final bool isSelected =
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
final bool isExcluded =
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
ColorFilter selectedFilter = ColorFilter.mode(
Theme.of(context).primaryColor.withAlpha(100),
BlendMode.darken,
);
ColorFilter excludedFilter =
ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
ColorFilter unselectedFilter =
const ColorFilter.mode(Colors.black, BlendMode.color);
var isDarkTheme = Theme.of(context).brightness == Brightness.dark;
var assetCount = useState(0);
useEffect(
() {
albumInfo.assetCount.then((value) => assetCount.value = value);
return null;
},
[],
);
buildImageFilter() {
if (isSelected) {
return selectedFilter;
} else if (isExcluded) {
return excludedFilter;
} else {
return unselectedFilter;
}
}
buildTileColor() {
if (isSelected) {
return isDarkTheme
? Theme.of(context).primaryColor.withAlpha(100)
: Theme.of(context).primaryColor.withAlpha(25);
} else if (isExcluded) {
return isDarkTheme
? Colors.red[300]?.withAlpha(150)
: Colors.red[100]?.withAlpha(150);
} else {
return Colors.transparent;
}
}
return GestureDetector(
onDoubleTap: () {
HapticFeedback.selectionClick();
if (isExcluded) {
// Remove from exclude album list
ref
.watch(backupProvider.notifier)
.removeExcludedAlbumForBackup(albumInfo);
} else {
// Add to exclude album list
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1 &&
ref
.watch(backupProvider)
.selectedBackupAlbums
.contains(albumInfo)) {
ImmichToast.show(
context: context,
msg: "backup_err_only_album".tr(),
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
return;
}
if (albumInfo.id == 'isAll' || albumInfo.name == 'Recents') {
ImmichToast.show(
context: context,
msg: 'Cannot exclude album contains all assets',
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
return;
}
ref
.watch(backupProvider.notifier)
.addExcludedAlbumForBackup(albumInfo);
}
},
child: ListTile(
tileColor: buildTileColor(),
contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
onTap: () {
HapticFeedback.selectionClick();
if (isSelected) {
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) {
ImmichToast.show(
context: context,
msg: "backup_err_only_album".tr(),
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
return;
}
ref.watch(backupProvider.notifier).removeAlbumForBackup(albumInfo);
} else {
ref.watch(backupProvider.notifier).addAlbumForBackup(albumInfo);
}
},
leading: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SizedBox(
height: 80,
width: 80,
child: ColorFiltered(
colorFilter: buildImageFilter(),
child: Image(
width: double.infinity,
height: double.infinity,
image: imageData != null
? MemoryImage(imageData!)
: const AssetImage(
'assets/immich-logo-no-outline.png',
) as ImageProvider,
fit: BoxFit.cover,
),
),
),
),
title: Text(
albumInfo.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
subtitle: Text(assetCount.value.toString()),
trailing: IconButton(
onPressed: () {
AutoRouter.of(context).push(
AlbumPreviewRoute(album: albumInfo.albumEntity),
);
},
icon: Icon(
Icons.image_outlined,
color: Theme.of(context).primaryColor,
size: 24,
),
splashRadius: 25,
),
),
);
}
}

View file

@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart'; import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/backup/ui/album_info_card.dart'; import 'package:immich_mobile/modules/backup/ui/album_info_card.dart';
import 'package:immich_mobile/modules/backup/ui/album_info_list_tile.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart';
@ -18,7 +19,12 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums; final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
final isDarkTheme = Theme.of(context).brightness == Brightness.dark; final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
final albums = ref.watch(backupProvider).availableAlbums; final allAlbums = ref.watch(backupProvider).availableAlbums;
// Albums which are displayed to the user
// by filtering out based on search
final filteredAlbums = useState(allAlbums);
final albums = filteredAlbums.value;
useEffect( useEffect(
() { () {
@ -30,27 +36,53 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
buildAlbumSelectionList() { buildAlbumSelectionList() {
if (albums.isEmpty) { if (albums.isEmpty) {
return const Center( return const SliverToBoxAdapter(
child: ImmichLoadingIndicator(), child: Center(
child: ImmichLoadingIndicator(),
),
); );
} }
return SizedBox( return SliverPadding(
height: 265, padding: const EdgeInsets.symmetric(vertical: 12.0),
child: ListView.builder( sliver: SliverList(
scrollDirection: Axis.horizontal, delegate: SliverChildBuilderDelegate(
itemCount: albums.length, ((context, index) {
physics: const BouncingScrollPhysics(), var thumbnailData = albums[index].thumbnailData;
itemBuilder: ((context, index) { return AlbumInfoListTile(
var thumbnailData = albums[index].thumbnailData;
return Padding(
padding: index == 0
? const EdgeInsets.only(left: 16.00)
: const EdgeInsets.all(0),
child: AlbumInfoCard(
imageData: thumbnailData, imageData: thumbnailData,
albumInfo: albums[index], albumInfo: albums[index],
), );
}),
childCount: albums.length,
),
),
);
}
buildAlbumSelectionGrid() {
if (albums.isEmpty) {
return const SliverToBoxAdapter(
child: Center(
child: ImmichLoadingIndicator(),
),
);
}
return SliverPadding(
padding: const EdgeInsets.all(12.0),
sliver: SliverGrid.builder(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 300,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
),
itemCount: albums.length,
itemBuilder: ((context, index) {
var thumbnailData = albums[index].thumbnailData;
return AlbumInfoCard(
imageData: thumbnailData,
albumInfo: albums[index],
); );
}), }),
), ),
@ -139,19 +171,17 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 8.0), padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 8.0),
child: TextFormField( child: TextFormField(
onChanged: (searchValue) { onChanged: (searchValue) {
var avaialbleAlbums = ref if (searchValue.isEmpty) {
.watch(backupProvider) filteredAlbums.value = allAlbums;
.availableAlbums } else {
.where( filteredAlbums.value = allAlbums
(album) => album.name .where(
.toLowerCase() (album) => album.name
.contains(searchValue.toLowerCase()), .toLowerCase()
) .contains(searchValue.toLowerCase()),
.toList(); )
.toList();
ref }
.read(backupProvider.notifier)
.setAvailableAlbums(avaialbleAlbums);
}, },
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
@ -190,143 +220,162 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
).tr(), ).tr(),
elevation: 0, elevation: 0,
), ),
body: ListView( body: CustomScrollView(
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
children: [ slivers: [
Padding( SliverToBoxAdapter(
padding: const EdgeInsets.symmetric( child: Column(
vertical: 8.0, crossAxisAlignment: CrossAxisAlignment.start,
horizontal: 16.0,
),
child: const Text(
"backup_album_selection_page_selection_info",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
).tr(),
),
// Selected Album Chips
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Wrap(
children: [ children: [
...buildSelectedAlbumNameChip(), Padding(
...buildExcludedAlbumNameChip() padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
child: const Text(
"backup_album_selection_page_selection_info",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
).tr(),
),
// Selected Album Chips
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Wrap(
children: [
...buildSelectedAlbumNameChip(),
...buildExcludedAlbumNameChip()
],
),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
child: Card(
margin: const EdgeInsets.all(0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: BorderSide(
color: isDarkTheme
? const Color.fromARGB(255, 0, 0, 0)
: const Color.fromARGB(255, 235, 235, 235),
width: 1,
),
),
elevation: 0,
borderOnForeground: false,
child: Column(
children: [
ListTile(
visualDensity: VisualDensity.compact,
title: const Text(
"backup_album_selection_page_total_assets",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
).tr(),
trailing: Text(
ref
.watch(backupProvider)
.allUniqueAssets
.length
.toString(),
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
),
),
ListTile(
title: Text(
"backup_album_selection_page_albums_device".tr(
args: [
ref
.watch(backupProvider)
.availableAlbums
.length
.toString()
],
),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
"backup_album_selection_page_albums_tap",
style: TextStyle(
fontSize: 12,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
).tr(),
),
trailing: IconButton(
splashRadius: 16,
icon: Icon(
Icons.info,
size: 20,
color: Theme.of(context).primaryColor,
),
onPressed: () {
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 5,
title: Text(
'backup_album_selection_page_selection_info',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
).tr(),
content: SingleChildScrollView(
child: ListBody(
children: [
const Text(
'backup_album_selection_page_assets_scatter',
style: TextStyle(
fontSize: 14,
),
).tr(),
],
),
),
);
},
);
},
),
),
buildSearchBar(),
], ],
), ),
), ),
SliverLayoutBuilder(
Padding( builder: (context, constraints) {
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), if (constraints.crossAxisExtent > 600) {
child: Card( return buildAlbumSelectionGrid();
margin: const EdgeInsets.all(0), } else {
shape: RoundedRectangleBorder( return buildAlbumSelectionList();
borderRadius: BorderRadius.circular(10), }
side: BorderSide( },
color: isDarkTheme
? const Color.fromARGB(255, 0, 0, 0)
: const Color.fromARGB(255, 235, 235, 235),
width: 1,
),
),
elevation: 0,
borderOnForeground: false,
child: Column(
children: [
ListTile(
visualDensity: VisualDensity.compact,
title: const Text(
"backup_album_selection_page_total_assets",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
).tr(),
trailing: Text(
ref
.watch(backupProvider)
.allUniqueAssets
.length
.toString(),
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
),
),
ListTile(
title: Text(
"backup_album_selection_page_albums_device".tr(
args: [
ref.watch(backupProvider).availableAlbums.length.toString()
],
),
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
"backup_album_selection_page_albums_tap",
style: TextStyle(
fontSize: 12,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
).tr(),
),
trailing: IconButton(
splashRadius: 16,
icon: Icon(
Icons.info,
size: 20,
color: Theme.of(context).primaryColor,
),
onPressed: () {
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 5,
title: Text(
'backup_album_selection_page_selection_info',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
).tr(),
content: SingleChildScrollView(
child: ListBody(
children: [
const Text(
'backup_album_selection_page_assets_scatter',
style: TextStyle(
fontSize: 14,
),
).tr(),
],
),
),
);
},
);
},
),
),
buildSearchBar(),
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: buildAlbumSelectionList(),
), ),
], ],
), ),

View file

@ -7,14 +7,18 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.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/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/login/providers/oauth.provider.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/routing/router.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/providers/asset.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/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/backup/providers/backup.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/shared/ui/immich_toast.dart';
import 'package:immich_mobile/utils/url_helper.dart'; import 'package:immich_mobile/utils/url_helper.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:permission_handler/permission_handler.dart';
class LoginForm extends HookConsumerWidget { class LoginForm extends HookConsumerWidget {
const LoginForm({Key? key}) : super(key: key); const LoginForm({Key? key}) : super(key: key);
@ -105,22 +109,12 @@ class LoginForm extends HookConsumerWidget {
onDoubleTap: () => populateTestLoginInfo(), onDoubleTap: () => populateTestLoginInfo(),
child: RotationTransition( child: RotationTransition(
turns: logoAnimationController, turns: logoAnimationController,
child: const Image( child: const ImmichLogo(
image: AssetImage('assets/immich-logo-no-outline.png'), heroTag: 'logo',
width: 100,
filterQuality: FilterQuality.high,
), ),
), ),
), ),
Text( const ImmichTitleText(),
'IMMICH',
style: TextStyle(
fontFamily: 'SnowburstOne',
fontWeight: FontWeight.bold,
fontSize: 48,
color: Theme.of(context).primaryColor,
),
),
EmailInput(controller: usernameController), EmailInput(controller: usernameController),
PasswordInput(controller: passwordController), PasswordInput(controller: passwordController),
ServerEndpointInput( ServerEndpointInput(
@ -164,7 +158,10 @@ class LoginForm extends HookConsumerWidget {
isLoading: isLoading, isLoading: isLoading,
onLoginSuccess: () { onLoginSuccess: () {
isLoading.value = false; 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( AutoRouter.of(context).replace(
const TabControllerRoute(), const TabControllerRoute(),
); );
@ -313,7 +310,13 @@ class LoginButton extends ConsumerWidget {
!ref.read(authenticationProvider).isAdmin) { !ref.read(authenticationProvider).isAdmin) {
AutoRouter.of(context).push(const ChangePasswordRoute()); AutoRouter.of(context).push(const ChangePasswordRoute());
} else { } 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()); AutoRouter.of(context).replace(const TabControllerRoute());
} }
} else { } else {

View file

@ -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());

View file

@ -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(),
);
},
),
],
),
),
),
),
);
}
}

View file

@ -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);
}
}

View file

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/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/services/app_settings.service.dart';
import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart'; import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';

View 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()]);
}
}
}

View file

@ -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/home/views/home_page.dart';
import 'package:immich_mobile/modules/login/views/change_password_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/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_page.dart';
import 'package:immich_mobile/modules/search/views/search_result_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/modules/settings/views/settings_page.dart';
import 'package:immich_mobile/routing/auth_guard.dart'; import 'package:immich_mobile/routing/auth_guard.dart';
import 'package:immich_mobile/routing/duplicate_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/asset.dart';
import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart';
@ -39,6 +42,7 @@ part 'router.gr.dart';
replaceInRouteName: 'Page,Route', replaceInRouteName: 'Page,Route',
routes: <AutoRoute>[ routes: <AutoRoute>[
AutoRoute(page: SplashScreenPage, initial: true), AutoRoute(page: SplashScreenPage, initial: true),
AutoRoute(page: PermissionOnboardingPage, guards: [AuthGuard, DuplicateGuard]),
AutoRoute(page: LoginPage, AutoRoute(page: LoginPage,
guards: [ guards: [
DuplicateGuard, DuplicateGuard,
@ -47,7 +51,7 @@ part 'router.gr.dart';
AutoRoute(page: ChangePasswordPage), AutoRoute(page: ChangePasswordPage),
CustomRoute( CustomRoute(
page: TabControllerPage, page: TabControllerPage,
guards: [AuthGuard, DuplicateGuard], guards: [AuthGuard, DuplicateGuard, GalleryPermissionGuard],
children: [ children: [
AutoRoute(page: HomePage, guards: [AuthGuard, DuplicateGuard]), AutoRoute(page: HomePage, guards: [AuthGuard, DuplicateGuard]),
AutoRoute(page: SearchPage, guards: [AuthGuard, DuplicateGuard]), AutoRoute(page: SearchPage, guards: [AuthGuard, DuplicateGuard]),
@ -56,7 +60,7 @@ part 'router.gr.dart';
], ],
transitionsBuilder: TransitionsBuilders.fadeIn, transitionsBuilder: TransitionsBuilders.fadeIn,
), ),
AutoRoute(page: GalleryViewerPage, guards: [AuthGuard, DuplicateGuard]), AutoRoute(page: GalleryViewerPage, guards: [AuthGuard, DuplicateGuard, GalleryPermissionGuard]),
AutoRoute(page: VideoViewerPage, guards: [AuthGuard, DuplicateGuard]), AutoRoute(page: VideoViewerPage, guards: [AuthGuard, DuplicateGuard]),
AutoRoute(page: BackupControllerPage, guards: [AuthGuard, DuplicateGuard]), AutoRoute(page: BackupControllerPage, guards: [AuthGuard, DuplicateGuard]),
AutoRoute(page: SearchResultPage, guards: [AuthGuard, DuplicateGuard]), AutoRoute(page: SearchResultPage, guards: [AuthGuard, DuplicateGuard]),
@ -101,12 +105,15 @@ class AppRouter extends _$AppRouter {
// ignore: unused_field // ignore: unused_field
final ApiService _apiService; final ApiService _apiService;
AppRouter(this._apiService) AppRouter(
: super( this._apiService,
GalleryPermissionNotifier galleryPermissionNotifier,
) : super(
authGuard: AuthGuard(_apiService), authGuard: AuthGuard(_apiService),
duplicateGuard: DuplicateGuard(), duplicateGuard: DuplicateGuard(),
galleryPermissionGuard: GalleryPermissionGuard(galleryPermissionNotifier),
); );
} }
final appRouterProvider = final appRouterProvider =
Provider((ref) => AppRouter(ref.watch(apiServiceProvider))); Provider((ref) => AppRouter(ref.watch(apiServiceProvider), ref.watch(galleryPermissionNotifier.notifier)));

View file

@ -15,13 +15,16 @@ part of 'router.dart';
class _$AppRouter extends RootStackRouter { class _$AppRouter extends RootStackRouter {
_$AppRouter({ _$AppRouter({
GlobalKey<NavigatorState>? navigatorKey, GlobalKey<NavigatorState>? navigatorKey,
required this.duplicateGuard,
required this.authGuard, required this.authGuard,
required this.duplicateGuard,
required this.galleryPermissionGuard,
}) : super(navigatorKey); }) : super(navigatorKey);
final AuthGuard authGuard;
final DuplicateGuard duplicateGuard; final DuplicateGuard duplicateGuard;
final AuthGuard authGuard; final GalleryPermissionGuard galleryPermissionGuard;
@override @override
final Map<String, PageFactory> pagesMap = { final Map<String, PageFactory> pagesMap = {
@ -31,6 +34,12 @@ class _$AppRouter extends RootStackRouter {
child: const SplashScreenPage(), child: const SplashScreenPage(),
); );
}, },
PermissionOnboardingRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData,
child: const PermissionOnboardingPage(),
);
},
LoginRoute.name: (routeData) { LoginRoute.name: (routeData) {
return MaterialPageX<dynamic>( return MaterialPageX<dynamic>(
routeData: routeData, routeData: routeData,
@ -225,6 +234,14 @@ class _$AppRouter extends RootStackRouter {
SplashScreenRoute.name, SplashScreenRoute.name,
path: '/', path: '/',
), ),
RouteConfig(
PermissionOnboardingRoute.name,
path: '/permission-onboarding-page',
guards: [
authGuard,
duplicateGuard,
],
),
RouteConfig( RouteConfig(
LoginRoute.name, LoginRoute.name,
path: '/login-page', path: '/login-page',
@ -240,6 +257,7 @@ class _$AppRouter extends RootStackRouter {
guards: [ guards: [
authGuard, authGuard,
duplicateGuard, duplicateGuard,
galleryPermissionGuard,
], ],
children: [ children: [
RouteConfig( RouteConfig(
@ -286,6 +304,7 @@ class _$AppRouter extends RootStackRouter {
guards: [ guards: [
authGuard, authGuard,
duplicateGuard, duplicateGuard,
galleryPermissionGuard,
], ],
), ),
RouteConfig( RouteConfig(
@ -411,6 +430,18 @@ class SplashScreenRoute extends PageRouteInfo<void> {
static const String name = 'SplashScreenRoute'; 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 /// generated route for
/// [LoginPage] /// [LoginPage]
class LoginRoute extends PageRouteInfo<void> { class LoginRoute extends PageRouteInfo<void> {

View 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,
),
);
}
}

View 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,
),
);
}
}

View file

@ -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/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.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/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/routing/router.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart';
@ -32,8 +33,13 @@ class SplashScreenPage extends HookConsumerWidget {
serverUrl: loginInfo.serverUrl, serverUrl: loginInfo.serverUrl,
); );
if (isSuccess) { if (isSuccess) {
// Resume backup (if enable) then navigate final hasPermission = await ref
ref.watch(backupProvider.notifier).resumeBackup(); .read(galleryPermissionNotifier.notifier)
.hasPermission;
if (hasPermission) {
// Resume backup (if enable) then navigate
ref.watch(backupProvider.notifier).resumeBackup();
}
AutoRouter.of(context).replace(const TabControllerRoute()); AutoRouter.of(context).replace(const TabControllerRoute());
} else { } else {
AutoRouter.of(context).replace(const LoginRoute()); AutoRouter.of(context).replace(const LoginRoute());

View file

@ -281,6 +281,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" 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: easy_image_viewer:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -46,6 +46,7 @@ dependencies:
isar: *isar_version isar: *isar_version
isar_flutter_libs: *isar_version # contains Isar Core isar_flutter_libs: *isar_version # contains Isar Core
permission_handler: ^10.2.0 permission_handler: ^10.2.0
device_info_plus: ^8.1.0
openapi: openapi:
path: openapi path: openapi