mirror of
https://github.com/immich-app/immich.git
synced 2025-08-07 09:04:09 -04:00
feat: view shared links inside of mobile app
This commit is contained in:
parent
f972b8d514
commit
c8c6f86518
21
mobile/lib/domain/models/album/shared_album.model.dart
Normal file
21
mobile/lib/domain/models/album/shared_album.model.dart
Normal 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,
|
||||
});
|
||||
}
|
14
mobile/lib/domain/services/remote_shared_album.service.dart
Normal file
14
mobile/lib/domain/services/remote_shared_album.service.dart
Normal 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);
|
||||
}
|
||||
}
|
@ -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'),
|
||||
};
|
||||
}
|
||||
|
@ -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})',
|
||||
|
@ -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() =>
|
||||
|
176
mobile/lib/presentation/pages/shared_remote_link.page.dart
Normal file
176
mobile/lib/presentation/pages/shared_remote_link.page.dart
Normal 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())),
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
6
mobile/lib/providers/api.provider.g.dart
generated
6
mobile/lib/providers/api.provider.g.dart
generated
@ -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')
|
||||
|
@ -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;
|
||||
|
@ -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> {
|
||||
|
@ -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>(
|
||||
|
@ -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'),
|
||||
};
|
||||
}
|
||||
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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: '/'),
|
||||
|
@ -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> {
|
||||
|
@ -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)) ?? []);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user