feat: view shared links inside of mobile app

This commit is contained in:
bwees 2025-07-31 13:38:14 -05:00
parent f972b8d514
commit c8c6f86518
No known key found for this signature in database
20 changed files with 434 additions and 61 deletions

View File

@ -0,0 +1,21 @@
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
class SharedRemoteAlbum extends RemoteAlbum {
final List<RemoteAsset> assets;
const SharedRemoteAlbum({
required super.id,
required this.assets,
required super.name,
required super.ownerId,
required super.description,
required super.createdAt,
required super.updatedAt,
super.thumbnailAssetId,
required super.isActivityEnabled,
required super.order,
required super.assetCount,
required super.ownerName,
});
}

View File

@ -0,0 +1,14 @@
import 'dart:async';
import 'package:immich_mobile/domain/models/album/shared_album.model.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
class RemoteSharedAlbumService {
final DriftAlbumApiRepository _albumApiRepository;
const RemoteSharedAlbumService(this._albumApiRepository);
Future<SharedRemoteAlbum?> getSharedAlbum(String albumId) {
return _albumApiRepository.getShared(albumId);
}
}

View File

@ -1,10 +1,9 @@
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/search_result.model.dart';
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart' as api show AssetVisibility;
import 'package:openapi/api.dart' hide AssetVisibility;
class SearchService {
@ -52,41 +51,3 @@ class SearchService {
return null;
}
}
extension on AssetResponseDto {
RemoteAsset toDto() {
return RemoteAsset(
id: id,
name: originalFileName,
checksum: checksum,
createdAt: fileCreatedAt,
updatedAt: updatedAt,
ownerId: ownerId,
visibility: switch (visibility) {
api.AssetVisibility.timeline => AssetVisibility.timeline,
api.AssetVisibility.hidden => AssetVisibility.hidden,
api.AssetVisibility.archive => AssetVisibility.archive,
api.AssetVisibility.locked => AssetVisibility.locked,
_ => AssetVisibility.timeline,
},
durationInSeconds: duration.toDuration()?.inSeconds ?? 0,
height: exifInfo?.exifImageHeight?.toInt(),
width: exifInfo?.exifImageWidth?.toInt(),
isFavorite: isFavorite,
livePhotoVideoId: livePhotoVideoId,
thumbHash: thumbhash,
localId: null,
type: type.toAssetType(),
);
}
}
extension on AssetTypeEnum {
AssetType toAssetType() => switch (this) {
AssetTypeEnum.IMAGE => AssetType.image,
AssetTypeEnum.VIDEO => AssetType.video,
AssetTypeEnum.AUDIO => AssetType.audio,
AssetTypeEnum.OTHER => AssetType.other,
_ => throw Exception('Unknown AssetType value: $this'),
};
}

View File

@ -158,6 +158,7 @@ class TimelineService {
BaseAsset getRandomAsset() => _buffer.elementAt(math.Random().nextInt(_buffer.length));
BaseAsset getAsset(int index) {
print("buffer len: " + _buffer.length.toString());
if (!hasRange(index, 1)) {
throw RangeError(
'TimelineService::getAsset Index $index not in buffer range [$_bufferOffset, ${_bufferOffset + _buffer.length})',

View File

@ -1,3 +1,5 @@
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:openapi/api.dart';
enum SharedLinkSource { album, individual }
@ -14,6 +16,8 @@ class SharedLink {
final String key;
final bool showMetadata;
final SharedLinkSource type;
final List<RemoteAsset> assets;
final String? albumId;
const SharedLink({
required this.id,
@ -27,6 +31,8 @@ class SharedLink {
required this.key,
required this.showMetadata,
required this.type,
this.assets = const [],
this.albumId,
});
SharedLink copyWith({
@ -74,7 +80,9 @@ class SharedLink {
? dto.album?.albumThumbnailAssetId
: dto.assets.isNotEmpty
? dto.assets[0].id
: null;
: null,
assets = dto.assets.map((asset) => asset.toDto()).toList(),
albumId = dto.album?.id;
@override
String toString() =>

View File

@ -0,0 +1,176 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/services/remote_shared_album.service.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/shared_link.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
// ignore: import_rule_openapi
import 'package:openapi/api.dart';
@RoutePage()
class RemoteSharedLinkPage extends ConsumerStatefulWidget {
final String shareKey;
final String endpoint;
const RemoteSharedLinkPage({super.key, required this.shareKey, required this.endpoint});
@override
ConsumerState<RemoteSharedLinkPage> createState() => _RemoteSharedLinkPageState();
}
class _RemoteSharedLinkPageState extends ConsumerState<RemoteSharedLinkPage> {
late final ApiService _apiService;
SharedLink? sharedLink;
List<RemoteAsset>? assets;
@override
void initState() {
super.initState();
String endpoint = widget.endpoint;
if (!endpoint.endsWith('/api')) {
endpoint += '/api';
}
ImageUrlBuilder.setHost(endpoint);
ImageUrlBuilder.setParameter('key', widget.shareKey);
_apiService = ApiService.shared(endpoint, widget.shareKey);
retrieveSharedLink();
}
@override
void dispose() {
ImageUrlBuilder.clear();
super.dispose();
}
Future<void> retrieveSharedLink() async {
try {
sharedLink = await SharedLinkService(_apiService).getMySharedLink();
} on ApiException catch (error, _) {
if (error.code == 401) {
// We need a password from user.
// TODO: make password input and try to auth again
}
}
if (sharedLink == null) {
ImmichToast.show(
context: context,
msg: "shared_link_not_found".t(context: context),
toastType: ToastType.error,
);
context.pop();
return;
}
// Retrieve assets from the shared link
switch (sharedLink!.type) {
case SharedLinkSource.album:
assets = await retrieveSharedAlbumAssets();
break;
case SharedLinkSource.individual:
assets = sharedLink!.assets;
break;
}
// if (assets!.isEmpty) {
// context.pop();
// }
setState(() {});
}
Future<List<RemoteAsset>> retrieveSharedAlbumAssets() async {
try {
final driftApiRepository = DriftAlbumApiRepository(_apiService.albumsApi);
final albumService = RemoteSharedAlbumService(driftApiRepository);
final sharedAlbum = await albumService.getSharedAlbum(sharedLink!.albumId!);
return sharedAlbum?.assets ?? [];
} catch (e) {
ImmichToast.show(
context: context,
msg: "failed_to_retrieve_assets".t(context: context),
toastType: ToastType.error,
);
return [];
}
}
Future<void> addAssets(BuildContext context) async {
// TODO add upload assets to shared album
}
void showOptionSheet(BuildContext context) {
final user = ref.watch(currentUserProvider);
// showModalBottomSheet(
// context: context,
// backgroundColor: context.colorScheme.surface,
// isScrollControlled: false,
// builder: (context) {
// return DriftRemoteAlbumOption(
// onDeleteAlbum: isOwner
// ? () async {
// await deleteAlbum(context);
// if (context.mounted) {
// context.pop();
// }
// }
// : null,
// onAddUsers: isOwner
// ? () async {
// await addUsers(context);
// context.pop();
// }
// : null,
// onAddPhotos: () async {
// await addAssets(context);
// context.pop();
// },
// onToggleAlbumOrder: () async {
// await toggleAlbumOrder();
// context.pop();
// },
// onEditAlbum: () async {
// context.pop();
// await showEditTitleAndDescription(context);
// },
// );
// },
// );
}
@override
Widget build(BuildContext context) {
if (assets == null || assets!.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
return ProviderScope(
overrides: [
apiServiceProvider.overrideWith((ref) => _apiService),
timelineServiceProvider.overrideWith((ref) {
print(assets!.length);
final timelineService = ref.watch(timelineFactoryProvider).fromAssets(assets!);
ref.onDispose(timelineService.dispose);
return timelineService;
}),
],
child: Timeline(appBar: SliverToBoxAdapter(child: AppBar())),
);
}
}

View File

@ -4,5 +4,5 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'api.provider.g.dart';
@Riverpod(keepAlive: true)
@Riverpod(keepAlive: true, dependencies: [])
ApiService apiService(Ref _) => ApiService();

View File

@ -6,7 +6,7 @@ part of 'api.provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$apiServiceHash() => r'187a7de59b064fab1104c23717f18ce0ae3e426c';
String _$apiServiceHash() => r'46d8a043f41b85f36f56d81e5261c842cb5c0c06';
/// See also [apiService].
@ProviderFor(apiService)
@ -16,8 +16,8 @@ final apiServiceProvider = Provider<ApiService>.internal(
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$apiServiceHash,
dependencies: null,
allTransitiveDependencies: null,
dependencies: const <ProviderOrFamily>[],
allTransitiveDependencies: const <ProviderOrFamily>{},
);
@Deprecated('Will be removed in 3.0. Use Ref instead')

View File

@ -28,7 +28,7 @@ final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
ref.watch(secureStorageServiceProvider),
ref.watch(widgetServiceProvider),
);
});
}, dependencies: [apiServiceProvider]);
class AuthNotifier extends StateNotifier<AuthState> {
final AuthService _authService;

View File

@ -2,10 +2,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart' as old_asset_entity;
import 'package:immich_mobile/models/cast/cast_manager_state.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/services/gcast.service.dart';
final castProvider = StateNotifierProvider<CastNotifier, CastManagerState>(
(ref) => CastNotifier(ref.watch(gCastServiceProvider)),
dependencies: [apiServiceProvider],
);
class CastNotifier extends StateNotifier<CastManagerState> {

View File

@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/services/local_album.service.dart';
import 'package:immich_mobile/domain/services/remote_album.service.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
@ -31,7 +32,7 @@ final remoteAlbumRepository = Provider<DriftRemoteAlbumRepository>(
final remoteAlbumServiceProvider = Provider<RemoteAlbumService>(
(ref) => RemoteAlbumService(ref.watch(remoteAlbumRepository), ref.watch(driftAlbumApiRepositoryProvider)),
dependencies: [remoteAlbumRepository],
dependencies: [remoteAlbumRepository, apiServiceProvider],
);
final remoteAlbumProvider = NotifierProvider<RemoteAlbumNotifier, RemoteAlbumState>(

View File

@ -1,12 +1,15 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/stack.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart' hide AssetType;
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:openapi/api.dart';
import 'package:openapi/api.dart' as api show AssetVisibility;
import 'package:openapi/api.dart' hide AssetVisibility;
final assetApiRepositoryProvider = Provider(
(ref) => AssetApiRepository(
@ -108,3 +111,41 @@ extension on StackResponseDto {
return StackResponse(id: id, primaryAssetId: primaryAssetId, assetIds: assets.map((asset) => asset.id).toList());
}
}
extension RemoteAssetDtoExt on AssetResponseDto {
RemoteAsset toDto() {
return RemoteAsset(
id: id,
name: originalFileName,
checksum: checksum,
createdAt: fileCreatedAt,
updatedAt: updatedAt,
ownerId: ownerId,
visibility: switch (visibility) {
api.AssetVisibility.timeline => AssetVisibility.timeline,
api.AssetVisibility.hidden => AssetVisibility.hidden,
api.AssetVisibility.archive => AssetVisibility.archive,
api.AssetVisibility.locked => AssetVisibility.locked,
_ => AssetVisibility.timeline,
},
durationInSeconds: duration.toDuration()?.inSeconds ?? 0,
height: exifInfo?.exifImageHeight?.toInt(),
width: exifInfo?.exifImageWidth?.toInt(),
isFavorite: isFavorite,
livePhotoVideoId: livePhotoVideoId,
thumbHash: thumbhash,
localId: null,
type: type.toType(),
);
}
}
extension on AssetTypeEnum {
AssetType toType() => switch (this) {
AssetTypeEnum.IMAGE => AssetType.image,
AssetTypeEnum.VIDEO => AssetType.video,
AssetTypeEnum.AUDIO => AssetType.audio,
AssetTypeEnum.OTHER => AssetType.other,
_ => throw Exception('Unknown AssetType value: $this'),
};
}

View File

@ -1,7 +1,9 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/album/shared_album.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
// ignore: import_rule_openapi
import 'package:openapi/api.dart';
@ -87,6 +89,11 @@ class DriftAlbumApiRepository extends ApiRepository {
final response = await checkNull(_api.addUsersToAlbum(albumId, AddUsersDto(albumUsers: albumUsers)));
return response.toRemoteAlbum();
}
Future<SharedRemoteAlbum?> getShared(String albumId) async {
final responseDto = await checkNull(_api.getAlbumInfo(albumId));
return responseDto.toSharedRemoteAlbum();
}
}
extension on AlbumResponseDto {
@ -105,4 +112,21 @@ extension on AlbumResponseDto {
ownerName: owner.name,
);
}
SharedRemoteAlbum toSharedRemoteAlbum() {
return SharedRemoteAlbum(
id: id,
name: albumName,
ownerId: owner.id,
description: description,
createdAt: createdAt,
updatedAt: updatedAt,
thumbnailAssetId: albumThumbnailAssetId,
isActivityEnabled: isActivityEnabled,
order: order == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc,
assetCount: assetCount,
ownerName: owner.name,
assets: assets.map((e) => e.toDto()).toList(),
);
}
}

View File

@ -6,6 +6,7 @@ import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
@ -17,6 +18,11 @@ class AuthGuard extends AutoRouteGuard {
void onNavigation(NavigationResolver resolver, StackRouter router) async {
resolver.next(true);
if (ImageUrlBuilder.isSharedLink()) {
// If the URL is a shared link, we don't need to validate the access token
return;
}
try {
// Look in the store for an access token
Store.get(StoreKey.accessToken);

View File

@ -23,11 +23,11 @@ import 'package:immich_mobile/pages/album/album_shared_user_selection.page.dart'
import 'package:immich_mobile/pages/album/album_viewer.page.dart';
import 'package:immich_mobile/pages/albums/albums.page.dart';
import 'package:immich_mobile/pages/backup/album_preview.page.dart';
import 'package:immich_mobile/pages/backup/drift_backup_album_selection.page.dart';
import 'package:immich_mobile/pages/backup/drift_backup.page.dart';
import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart';
import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
import 'package:immich_mobile/pages/backup/backup_options.page.dart';
import 'package:immich_mobile/pages/backup/drift_backup.page.dart';
import 'package:immich_mobile/pages/backup/drift_backup_album_selection.page.dart';
import 'package:immich_mobile/pages/backup/drift_backup_options.page.dart';
import 'package:immich_mobile/pages/backup/drift_upload_detail.page.dart';
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
@ -95,12 +95,13 @@ import 'package:immich_mobile/presentation/pages/drift_person.page.dart';
import 'package:immich_mobile/presentation/pages/drift_place.page.dart';
import 'package:immich_mobile/presentation/pages/drift_place_detail.page.dart';
import 'package:immich_mobile/presentation/pages/drift_recently_taken.page.dart';
import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart';
import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart';
import 'package:immich_mobile/presentation/pages/drift_trash.page.dart';
import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart';
import 'package:immich_mobile/presentation/pages/drift_video.page.dart';
import 'package:immich_mobile/presentation/pages/local_timeline.page.dart';
import 'package:immich_mobile/presentation/pages/search/drift_search.page.dart';
import 'package:immich_mobile/presentation/pages/shared_remote_link.page.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
@ -329,6 +330,7 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: DriftPeopleCollectionRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftPersonRoute.page, guards: [_authGuard]),
AutoRoute(page: DriftBackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: RemoteSharedLinkRoute.page, guards: [_authGuard, _duplicateGuard]),
// required to handle all deeplinks in deep_link.service.dart
// auto_route_library#1722
RedirectRoute(path: '*', redirectTo: '/'),

View File

@ -2167,6 +2167,58 @@ class RemoteMediaSummaryRoute extends PageRouteInfo<void> {
);
}
/// generated route for
/// [RemoteSharedLinkPage]
class RemoteSharedLinkRoute extends PageRouteInfo<RemoteSharedLinkRouteArgs> {
RemoteSharedLinkRoute({
Key? key,
required String shareKey,
required String endpoint,
List<PageRouteInfo>? children,
}) : super(
RemoteSharedLinkRoute.name,
args: RemoteSharedLinkRouteArgs(
key: key,
shareKey: shareKey,
endpoint: endpoint,
),
initialChildren: children,
);
static const String name = 'RemoteSharedLinkRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
final args = data.argsAs<RemoteSharedLinkRouteArgs>();
return RemoteSharedLinkPage(
key: args.key,
shareKey: args.shareKey,
endpoint: args.endpoint,
);
},
);
}
class RemoteSharedLinkRouteArgs {
const RemoteSharedLinkRouteArgs({
this.key,
required this.shareKey,
required this.endpoint,
});
final Key? key;
final String shareKey;
final String endpoint;
@override
String toString() {
return 'RemoteSharedLinkRouteArgs{key: $key, shareKey: $shareKey, endpoint: $endpoint}';
}
}
/// generated route for
/// [SearchPage]
class SearchRoute extends PageRouteInfo<SearchRouteArgs> {

View File

@ -8,9 +8,9 @@ import 'package:http/http.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/utils/url_helper.dart';
import 'package:immich_mobile/utils/user_agent.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/user_agent.dart';
class ApiService implements Authentication {
late ApiClient _apiClient;
@ -45,7 +45,14 @@ class ApiService implements Authentication {
setEndpoint(endpoint);
}
}
ApiService.shared(String endpoint, String sharedKey) {
setEndpoint(endpoint);
_queryParams = {'key': sharedKey};
}
String? _accessToken;
Map<String, String>? _queryParams;
final _log = Logger("ApiService");
setEndpoint(String endpoint) {
@ -208,6 +215,8 @@ class ApiService implements Authentication {
return Future<void>(() {
var headers = ApiService.getRequestHeaders();
headerParams.addAll(headers);
queryParams.addAll(_queryParams?.entries.map((e) => QueryParam(e.key, e.value)) ?? []);
});
}

View File

@ -6,8 +6,8 @@ import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/entities/store.entity.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/asset.provider.dart' as beta_asset_provider;
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/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
@ -80,6 +80,7 @@ class DeepLinkService {
"memory" => await _buildMemoryDeepLink(queryParams['id'] ?? ''),
"asset" => await _buildAssetDeepLink(queryParams['id'] ?? ''),
"album" => await _buildAlbumDeepLink(queryParams['id'] ?? ''),
"sharedlink" => await _buildSharedLinkDeepLink(queryParams['key'] ?? '', queryParams['instanceUrl'] ?? ''),
_ => null,
};
@ -99,18 +100,24 @@ class DeepLinkService {
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}';
const b64Regex = r'^[A-Za-z0-9_-]+$';
final assetRegex = RegExp('/photos/($uuidRegex)');
final albumRegex = RegExp('/albums/($uuidRegex)');
final sharedLinkRegex = RegExp('/share/($b64Regex)');
PageRouteInfo<dynamic>? deepLinkRoute;
if (assetRegex.hasMatch(path)) {
final assetId = assetRegex.firstMatch(path)?.group(1) ?? '';
deepLinkRoute = await _buildAssetDeepLink(assetId);
} else if (albumRegex.hasMatch(path)) {
final albumId = albumRegex.firstMatch(path)?.group(1) ?? '';
deepLinkRoute = await _buildAlbumDeepLink(albumId);
} else if (sharedLinkRegex.hasMatch(path)) {
final shareKey = sharedLinkRegex.firstMatch(path)?.group(1) ?? '';
final instanceUrl = link.uri.queryParameters['instanceUrl'] ?? '';
deepLinkRoute = await _buildSharedLinkDeepLink(shareKey, instanceUrl);
}
// Deep link resolution failed, safely handle it based on the app state
if (deepLinkRoute == null) {
if (isColdStart) return DeepLink.defaultPath;
@ -185,4 +192,8 @@ class DeepLinkService {
return AlbumViewerRoute(albumId: album.id);
}
}
Future<PageRouteInfo?> _buildSharedLinkDeepLink(String shareKey, String instanceUrl) async {
return RemoteSharedLinkRoute(shareKey: shareKey, endpoint: instanceUrl);
}
}

View File

@ -111,4 +111,13 @@ class SharedLinkService {
}
return null;
}
Future<SharedLink?> getMySharedLink() async {
final responseDto = await _apiService.sharedLinksApi.getMySharedLink();
if (responseDto != null) {
return SharedLink.fromDto(responseDto);
}
return null;
}
}

View File

@ -36,7 +36,7 @@ String getAlbumThumbNailCacheKey(final Album album, {AssetMediaSize type = Asset
}
String getOriginalUrlForRemoteId(final String id) {
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/original';
return ImageUrlBuilder.build('/assets/$id/original');
}
String getImageCacheKey(final Asset asset) {
@ -46,16 +46,51 @@ String getImageCacheKey(final Asset asset) {
}
String getThumbnailUrlForRemoteId(final String id, {AssetMediaSize type = AssetMediaSize.thumbnail}) {
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.value}';
return ImageUrlBuilder.build('/assets/$id/thumbnail?size=${type.value}');
}
String getPreviewUrlForRemoteId(final String id) =>
'${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${AssetMediaSize.preview}';
String getPreviewUrlForRemoteId(final String id) {
return ImageUrlBuilder.build('/assets/$id/thumbnail?size=${AssetMediaSize.preview}');
}
String getPlaybackUrlForRemoteId(final String id) {
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/video/playback?';
return ImageUrlBuilder.build('/assets/$id/video/playback?');
}
String getFaceThumbnailUrl(final String personId) {
return '${Store.get(StoreKey.serverEndpoint)}/people/$personId/thumbnail';
return ImageUrlBuilder.build('/people/$personId/thumbnail');
}
class ImageUrlBuilder {
static String? host;
static Map<String, String>? queryParams;
static void setHost(String? host) {
ImageUrlBuilder.host = host;
}
static bool isSharedLink() {
return ImageUrlBuilder.host != null && ImageUrlBuilder.queryParams!.containsKey('key');
}
static void setParameter(String key, String value) {
ImageUrlBuilder.queryParams ??= {};
ImageUrlBuilder.queryParams![key] = value;
}
static String build(String path) {
final endpoint = host ?? Store.get(StoreKey.serverEndpoint);
final uri = Uri.parse('$endpoint$path');
if (queryParams == null || queryParams!.isEmpty) {
return uri.toString();
}
final updatedUri = uri.replace(queryParameters: {...uri.queryParameters, ...queryParams!});
return updatedUri.toString();
}
static void clear() {
ImageUrlBuilder.host = null;
ImageUrlBuilder.queryParams = null;
}
}