diff --git a/mobile-v2/.gitignore b/mobile-v2/.gitignore index 7846f3208f..617250fec0 100644 --- a/mobile-v2/.gitignore +++ b/mobile-v2/.gitignore @@ -3,6 +3,8 @@ *.gr.dart *.drift.dart *.gen.dart +*.g.swift +*.g.kt openapi/* # Miscellaneous diff --git a/mobile-v2/android/app/src/main/AndroidManifest.xml b/mobile-v2/android/app/src/main/AndroidManifest.xml index bc753bd1f3..1df1b0843b 100644 --- a/mobile-v2/android/app/src/main/AndroidManifest.xml +++ b/mobile-v2/android/app/src/main/AndroidManifest.xml @@ -1,8 +1,23 @@ + + + + + + + + + + + android:label="Immich" + android:name=".ImmichApp" + android:icon="@mipmap/ic_launcher" + android:usesCleartextTraffic="true" + android:requestLegacyExternalStorage="true" + android:largeHeap="true" + android:enableOnBackInvokedCallback="true" + > 3.45.3+1)" + - "sqlite3 (~> 3.46.0+1)" + - sqlite3/dbstatvtab - sqlite3/fts5 - sqlite3/perf-threadsafe - sqlite3/rtree + - url_launcher_ios (0.0.1): + - Flutter DEPENDENCIES: + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - Flutter (from `Flutter`) + - flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - photo_manager (from `.symlinks/plugins/photo_manager/ios`) + - sqflite (from `.symlinks/plugins/sqflite/darwin`) - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: trunk: - sqlite3 EXTERNAL SOURCES: + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" Flutter: :path: Flutter + flutter_web_auth_2: + :path: ".symlinks/plugins/flutter_web_auth_2/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" photo_manager: :path: ".symlinks/plugins/photo_manager/ios" + sqflite: + :path: ".symlinks/plugins/sqflite/darwin" sqlite3_flutter_libs: :path: ".symlinks/plugins/sqlite3_flutter_libs/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: + device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_web_auth_2: 051cf9f5dc366f31b5dcc4e2952c2b954767be8a + package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a - sqlite3: 02d1f07eaaa01f80a1c16b4b31dfcbb3345ee01a - sqlite3_flutter_libs: 9bfe005308998aeca155330bbc2ea6dddf834a3b + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb + sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d diff --git a/mobile-v2/ios/Runner/Info.plist b/mobile-v2/ios/Runner/Info.plist index 6a253c326a..5b5014f836 100644 --- a/mobile-v2/ios/Runner/Info.plist +++ b/mobile-v2/ios/Runner/Info.plist @@ -57,5 +57,9 @@ https + + NSPhotoLibraryUsageDescription + In order to access iOS photo library for local assets and albums + diff --git a/mobile-v2/lib/domain/repositories/database.repository.dart b/mobile-v2/lib/domain/repositories/database.repository.dart index aeb99befa1..6676d91866 100644 --- a/mobile-v2/lib/domain/repositories/database.repository.dart +++ b/mobile-v2/lib/domain/repositories/database.repository.dart @@ -14,7 +14,11 @@ import 'database.repository.drift.dart'; @DriftDatabase(tables: [Logs, Store, LocalAlbum, Asset, User]) class DriftDatabaseRepository extends $DriftDatabaseRepository { DriftDatabaseRepository([QueryExecutor? executor]) - : super(executor ?? driftDatabase(name: 'db')); + : super(executor ?? + driftDatabase( + name: 'db', + native: const DriftNativeOptions(shareAcrossIsolates: true), + )); @override int get schemaVersion => 1; diff --git a/mobile-v2/lib/domain/services/login.service.dart b/mobile-v2/lib/domain/services/login.service.dart index 7c77241512..db019d0051 100644 --- a/mobile-v2/lib/domain/services/login.service.dart +++ b/mobile-v2/lib/domain/services/login.service.dart @@ -4,6 +4,9 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:http/http.dart'; +import 'package:immich_mobile/domain/interfaces/store.interface.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/service_locator.dart'; import 'package:immich_mobile/utils/immich_api_client.dart'; import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; @@ -103,4 +106,32 @@ class LoginService with LogContext { } return null; } + + Future tryLoginFromSplash() async { + final serverEndpoint = + await di().tryGet(StoreKey.serverEndpoint); + if (serverEndpoint == null) { + return false; + } + + ServiceLocator.registerApiClient(serverEndpoint); + ServiceLocator.registerPostValidationServices(); + ServiceLocator.registerPostGlobalStates(); + + final accessToken = + await di().tryGet(StoreKey.accessToken); + if (accessToken == null) { + return false; + } + + /// Set token to interceptor + await di().init(accessToken: accessToken); + + final user = await di().getMyUser(); + if (user == null) { + return false; + } + + return true; + } } diff --git a/mobile-v2/lib/domain/services/sync.service.dart b/mobile-v2/lib/domain/services/sync.service.dart index 6cff50897f..c7413f0e9a 100644 --- a/mobile-v2/lib/domain/services/sync.service.dart +++ b/mobile-v2/lib/domain/services/sync.service.dart @@ -1,4 +1,5 @@ -import 'package:drift/isolate.dart'; +import 'dart:async'; + import 'package:immich_mobile/domain/interfaces/asset.interface.dart'; import 'package:immich_mobile/domain/models/asset.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; @@ -12,56 +13,51 @@ import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; class SyncService with LogContext { - final DriftDatabaseRepository _db; - - SyncService(this._db); + SyncService(); Future doFullSyncForUserDrift( User user, { DateTime? updatedUtil, int? limit, }) async { - final helper = IsolateHelper()..preIsolateHandling(); - try { - await _db.computeWithDatabase( - connect: (connection) => DriftDatabaseRepository(connection), - computation: (database) async { - helper.postIsolateHandling(database: database); - final logger = Logger("SyncService "); - final syncClient = di().getSyncApi(); + return await IsolateHelper.run(() async { + try { + final logger = Logger("SyncService "); + final syncClient = di().getSyncApi(); - final chunkSize = limit ?? kFullSyncChunkSize; - final updatedTill = updatedUtil ?? DateTime.now().toUtc(); - updatedUtil ??= DateTime.now().toUtc(); - String? lastAssetId; + final chunkSize = limit ?? kFullSyncChunkSize; + final updatedTill = updatedUtil ?? DateTime.now().toUtc(); + updatedUtil ??= DateTime.now().toUtc(); + String? lastAssetId; - while (true) { - logger.info( - "Requesting more chunks from lastId - ${lastAssetId ?? ""}", - ); + while (true) { + logger.info( + "Requesting more chunks from lastId - ${lastAssetId ?? ""}", + ); - final assets = await syncClient.getFullSyncForUser(AssetFullSyncDto( - limit: chunkSize, - updatedUntil: updatedTill, - lastId: lastAssetId, - userId: user.id, - )); - if (assets == null) { - break; - } - - await di().addAll(assets.map(Asset.remote)); - - lastAssetId = assets.lastOrNull?.id; - if (assets.length != chunkSize) break; + final assets = await syncClient.getFullSyncForUser(AssetFullSyncDto( + limit: chunkSize, + updatedUntil: updatedTill, + lastId: lastAssetId, + userId: user.id, + )); + if (assets == null) { + break; } - return true; - }, - ); - } catch (e, s) { - log.severe("Error performing full sync for user - ${user.name}", e, s); - } - return false; + await di().addAll(assets.map(Asset.remote)); + + lastAssetId = assets.lastOrNull?.id; + if (assets.length != chunkSize) break; + } + + return true; + } catch (e, s) { + log.severe("Error performing full sync for user - ${user.name}", e, s); + } finally { + await di().close(); + } + return false; + }); } } diff --git a/mobile-v2/lib/domain/services/user.service.dart b/mobile-v2/lib/domain/services/user.service.dart index 4640f02ed9..95376e4f5b 100644 --- a/mobile-v2/lib/domain/services/user.service.dart +++ b/mobile-v2/lib/domain/services/user.service.dart @@ -9,13 +9,19 @@ class UserService with LogContext { Future getMyUser() async { try { - final userDto = await _userApi.getMyUser(); + final [ + userDto as UserAdminResponseDto?, + preferencesDto as UserPreferencesResponseDto? + ] = await Future.wait([ + _userApi.getMyUser(), + _userApi.getMyPreferences(), + ]); + if (userDto == null) { log.severe("Cannot fetch my user."); return null; } - final preferencesDto = await _userApi.getMyPreferences(); return User.fromAdminDto(userDto, preferencesDto); } catch (e, s) { log.severe("Error while fetching my user", e, s); diff --git a/mobile-v2/lib/presentation/router/pages/splash_screen.page.dart b/mobile-v2/lib/presentation/router/pages/splash_screen.page.dart index 389b350d95..b7b6ecddd4 100644 --- a/mobile-v2/lib/presentation/router/pages/splash_screen.page.dart +++ b/mobile-v2/lib/presentation/router/pages/splash_screen.page.dart @@ -1,6 +1,9 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:immich_mobile/domain/services/login.service.dart'; import 'package:immich_mobile/presentation/components/image/immich_logo.widget.dart'; import 'package:immich_mobile/presentation/modules/login/states/login_page.state.dart'; import 'package:immich_mobile/presentation/router/router.dart'; @@ -45,6 +48,14 @@ class _SplashScreenState extends State super.dispose(); } + Future _tryLogin() async { + if (await di().tryLoginFromSplash() && mounted) { + unawaited(context.replaceRoute(const TabControllerRoute())); + } else { + unawaited(context.replaceRoute(const LoginRoute())); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -52,7 +63,7 @@ class _SplashScreenState extends State future: di.allReady(), builder: (_, snap) { if (snap.hasData) { - context.replaceRoute(const LoginRoute()); + _tryLogin(); } else if (snap.hasError) { log.severe( "Error while initializing the app", diff --git a/mobile-v2/lib/service_locator.dart b/mobile-v2/lib/service_locator.dart index 705db3a901..5f64a38dee 100644 --- a/mobile-v2/lib/service_locator.dart +++ b/mobile-v2/lib/service_locator.dart @@ -92,7 +92,7 @@ class ServiceLocator { _registerFactory(() => ServerInfoService( di().getServerApi(), )); - _registerFactory(() => SyncService(di())); + _registerFactory(() => SyncService()); } static void registerPostGlobalStates() { diff --git a/mobile-v2/lib/utils/isolate_helper.dart b/mobile-v2/lib/utils/isolate_helper.dart index 76d42b4f6c..cda3477bb5 100644 --- a/mobile-v2/lib/utils/isolate_helper.dart +++ b/mobile-v2/lib/utils/isolate_helper.dart @@ -1,4 +1,8 @@ +import 'dart:async'; +import 'dart:isolate'; + import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:immich_mobile/domain/repositories/database.repository.dart'; import 'package:immich_mobile/service_locator.dart'; import 'package:immich_mobile/utils/immich_api_client.dart'; @@ -12,10 +16,13 @@ class _ImApiClientData { const _ImApiClientData({required this.endpoint, required this.headersMap}); } +// !! Should be used only from the root isolate class IsolateHelper { // Cache the ApiClient to reconstruct it later after inside the isolate late final _ImApiClientData? _clientData; + static RootIsolateToken get _rootToken => RootIsolateToken.instance!; + IsolateHelper(); void preIsolateHandling() { @@ -26,7 +33,7 @@ class IsolateHelper { ); } - void postIsolateHandling({required DriftDatabaseRepository database}) { + void postIsolateHandling() { assert(_clientData != null); // Reconstruct client from cached data final client = ImmichApiClient(endpoint: _clientData!.endpoint); @@ -36,11 +43,21 @@ class IsolateHelper { // Register all services in the isolates memory ServiceLocator.configureServicesForIsolate( - database: database, + database: DriftDatabaseRepository(), apiClient: client, ); // Init log manager to continue listening to log events LogManager.I.init(); } + + static Future run(FutureOr Function() computation) async { + final helper = IsolateHelper()..preIsolateHandling(); + final token = _rootToken; + return await Isolate.run(() async { + BackgroundIsolateBinaryMessenger.ensureInitialized(token); + helper.postIsolateHandling(); + return await computation(); + }); + } } diff --git a/mobile-v2/pubspec.lock b/mobile-v2/pubspec.lock index 4a19dd2aef..f71b5eca8b 100644 --- a/mobile-v2/pubspec.lock +++ b/mobile-v2/pubspec.lock @@ -359,10 +359,10 @@ packages: dependency: "direct main" description: name: flutter_adaptive_scaffold - sha256: "6b587d439c7da037432bbfc78d9676e1d08f2d7490f08e8d689a20f08e049802" + sha256: "8c515a2cb8abb3a567f8e77f10b33f47bb6fcadfe31f62364e0aca36280cdf93" url: "https://pub.dev" source: hosted - version: "0.2.6" + version: "0.3.1" flutter_bloc: dependency: "direct main" description: @@ -769,6 +769,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + pigeon: + dependency: "direct dev" + description: + name: pigeon + sha256: "95481446c02fa79fcf0e8014882f8a3b87fd06c257e9e1c3d4cc6d102a925ad8" + url: "https://pub.dev" + source: hosted + version: "22.4.0" platform: dependency: transitive description: diff --git a/mobile-v2/pubspec.yaml b/mobile-v2/pubspec.yaml index b17ed90f8a..0314d223cb 100644 --- a/mobile-v2/pubspec.yaml +++ b/mobile-v2/pubspec.yaml @@ -48,7 +48,7 @@ dependencies: flutter_web_auth_2: ^3.1.2 # components material_symbols_icons: ^4.2785.1 - flutter_adaptive_scaffold: ^0.2.6 + flutter_adaptive_scaffold: ^0.3.1 scrollable_positioned_list: ^0.3.8 cached_network_image: ^3.4.1 flutter_cache_manager: ^3.4.1 @@ -77,6 +77,8 @@ dev_dependencies: slang_build_runner: ^3.31.0 # Assets constant generator flutter_gen_runner: ^5.7.0 + # Type safe platform channels + pigeon: ^22.4.0 flutter: uses-material-design: true