import 'package:auto_route/auto_route.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/asset.service.dart' as beta_asset_service; import 'package:immich_mobile/domain/services/memory.service.dart'; import 'package:immich_mobile/domain/services/people.service.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart' as beta_asset_provider; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/asset.service.dart'; import 'package:immich_mobile/services/memory.service.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; final deepLinkServiceProvider = Provider( (ref) => DeepLinkService( ref.watch(memoryServiceProvider), ref.watch(assetServiceProvider), ref.watch(albumServiceProvider), ref.watch(currentAssetProvider.notifier), ref.watch(currentAlbumProvider.notifier), // Below is used for beta timeline ref.watch(timelineFactoryProvider), ref.watch(beta_asset_provider.assetServiceProvider), ref.watch(remoteAlbumServiceProvider), ref.watch(driftMemoryServiceProvider), ref.watch(driftPeopleServiceProvider), ref.watch(currentUserProvider), ), ); class DeepLinkService { /// TODO: Remove this when beta is default final MemoryService _memoryService; final AssetService _assetService; final AlbumService _albumService; final CurrentAsset _currentAsset; final CurrentAlbum _currentAlbum; /// Used for beta timeline final TimelineFactory _betaTimelineFactory; final beta_asset_service.AssetService _betaAssetService; final RemoteAlbumService _betaRemoteAlbumService; final DriftMemoryService _betaMemoryService; final DriftPeopleService _betaPeopleService; final UserDto? _currentUser; const DeepLinkService( this._memoryService, this._assetService, this._albumService, this._currentAsset, this._currentAlbum, this._betaTimelineFactory, this._betaAssetService, this._betaRemoteAlbumService, this._betaMemoryService, this._betaPeopleService, this._currentUser, ); DeepLink _handleColdStart(PageRouteInfo route, bool isColdStart) { return DeepLink([ // we need something to segue back to if the app was cold started // TODO: use MainTimelineRoute this when beta is default if (isColdStart) (Store.isBetaTimelineEnabled) ? const TabShellRoute() : const PhotosRoute(), route, ]); } Future handleScheme(PlatformDeepLink link, WidgetRef ref, bool isColdStart) async { // get everything after the scheme, since Uri cannot parse path final intent = link.uri.host; final queryParams = link.uri.queryParameters; PageRouteInfo? deepLinkRoute = switch (intent) { "memory" => await _buildMemoryDeepLink(queryParams['id'] ?? ''), "asset" => await _buildAssetDeepLink(queryParams['id'] ?? '', ref), "album" => await _buildAlbumDeepLink(queryParams['id'] ?? ''), "people" => await _buildPeopleDeepLink(queryParams['id'] ?? ''), "activity" => await _buildActivityDeepLink(queryParams['albumId'] ?? ''), _ => null, }; // Deep link resolution failed, safely handle it based on the app state if (deepLinkRoute == null) { if (isColdStart) { return DeepLink.defaultPath; } return DeepLink.none; } return _handleColdStart(deepLinkRoute, isColdStart); } Future handleMyImmichApp(PlatformDeepLink link, WidgetRef ref, bool isColdStart) async { final path = link.uri.path; const uuidRegex = r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'; final assetRegex = RegExp('/photos/($uuidRegex)'); final albumRegex = RegExp('/albums/($uuidRegex)'); final peopleRegex = RegExp('/people/($uuidRegex)'); PageRouteInfo? deepLinkRoute; if (assetRegex.hasMatch(path)) { final assetId = assetRegex.firstMatch(path)?.group(1) ?? ''; deepLinkRoute = await _buildAssetDeepLink(assetId, ref); } else if (albumRegex.hasMatch(path)) { final albumId = albumRegex.firstMatch(path)?.group(1) ?? ''; deepLinkRoute = await _buildAlbumDeepLink(albumId); } else if (peopleRegex.hasMatch(path)) { final peopleId = peopleRegex.firstMatch(path)?.group(1) ?? ''; deepLinkRoute = await _buildPeopleDeepLink(peopleId); } else if (path == "/memory") { deepLinkRoute = await _buildMemoryDeepLink(null); } // Deep link resolution failed, safely handle it based on the app state if (deepLinkRoute == null) { if (isColdStart) return DeepLink.defaultPath; return DeepLink.none; } return _handleColdStart(deepLinkRoute, isColdStart); } Future _buildMemoryDeepLink(String? memoryId) async { if (Store.isBetaTimelineEnabled) { List memories = []; if (memoryId == null) { if (_currentUser == null) { return null; } memories = await _betaMemoryService.getMemoryLane(_currentUser.id); } else { final memory = await _betaMemoryService.get(memoryId); if (memory != null) { memories = [memory]; } } if (memories.isEmpty) { return null; } return DriftMemoryRoute(memories: memories, memoryIndex: 0); } else { // TODO: Remove this when beta is default if (memoryId == null) { return null; } final memory = await _memoryService.getMemoryById(memoryId); if (memory == null) { return null; } return MemoryRoute(memories: [memory], memoryIndex: 0); } } Future _buildAssetDeepLink(String assetId, WidgetRef ref) async { if (Store.isBetaTimelineEnabled) { final asset = await _betaAssetService.getRemoteAsset(assetId); if (asset == null) { return null; } AssetViewer.setAsset(ref, asset); return AssetViewerRoute( initialIndex: 0, timelineService: _betaTimelineFactory.fromAssets([asset], TimelineOrigin.deepLink), ); } else { // TODO: Remove this when beta is default final asset = await _assetService.getAssetByRemoteId(assetId); if (asset == null) { return null; } _currentAsset.set(asset); final renderList = await RenderList.fromAssets([asset], GroupAssetsBy.auto); return GalleryViewerRoute(renderList: renderList, initialIndex: 0, heroOffset: 0, showStack: true); } } Future _buildAlbumDeepLink(String albumId) async { if (Store.isBetaTimelineEnabled) { final album = await _betaRemoteAlbumService.get(albumId); if (album == null) { return null; } return RemoteAlbumRoute(album: album); } else { // TODO: Remove this when beta is default final album = await _albumService.getAlbumByRemoteId(albumId); if (album == null) { return null; } _currentAlbum.set(album); return AlbumViewerRoute(albumId: album.id); } } Future _buildActivityDeepLink(String albumId) async { if (Store.isBetaTimelineEnabled == false) { return null; } final album = await _betaRemoteAlbumService.get(albumId); if (album == null || album.isActivityEnabled == false) { return null; } return DriftActivitiesRoute(album: album); } Future _buildPeopleDeepLink(String personId) async { if (Store.isBetaTimelineEnabled == false) { return null; } final person = await _betaPeopleService.get(personId); if (person == null) { return null; } return DriftPersonRoute(person: person); } }