mirror of
https://github.com/immich-app/immich.git
synced 2026-06-13 11:31:46 -07:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b68949502 | |||
| f86cb94736 | |||
| b21af78454 | |||
| abd62d9295 |
@@ -70,7 +70,10 @@ class DeepLinkService {
|
||||
|
||||
if (assetRegex.hasMatch(path)) {
|
||||
final assetId = assetRegex.firstMatch(path)?.group(1) ?? '';
|
||||
return _buildAssetDeepLink(assetId, ref);
|
||||
// /albums/<albumId>/photos/<assetId> links carry the album context,
|
||||
// which drives the like/comment UI in the viewer
|
||||
final albumId = albumRegex.firstMatch(path)?.group(1);
|
||||
return _buildAssetDeepLink(assetId, ref, albumId: albumId);
|
||||
}
|
||||
if (albumRegex.hasMatch(path)) {
|
||||
final albumId = albumRegex.firstMatch(path)?.group(1) ?? '';
|
||||
@@ -107,16 +110,19 @@ class DeepLinkService {
|
||||
return DriftMemoryRoute(memories: memories, memoryIndex: 0);
|
||||
}
|
||||
|
||||
Future<PageRouteInfo?> _buildAssetDeepLink(String assetId, WidgetRef ref) async {
|
||||
Future<PageRouteInfo?> _buildAssetDeepLink(String assetId, WidgetRef ref, {String? albumId}) async {
|
||||
final asset = await _betaAssetService.getRemoteAsset(assetId);
|
||||
if (asset == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final album = albumId != null ? await _betaRemoteAlbumService.get(albumId) : null;
|
||||
|
||||
AssetViewer.setAsset(ref, asset);
|
||||
return AssetViewerRoute(
|
||||
initialIndex: 0,
|
||||
timelineService: _betaTimelineFactory.fromAssets([asset], TimelineOrigin.deepLink),
|
||||
currentAlbum: album,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/domain/services/asset.service.dart';
|
||||
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/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/deep_link.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockTimelineFactory extends Mock implements TimelineFactory {}
|
||||
|
||||
class MockAssetService extends Mock implements AssetService {}
|
||||
|
||||
class MockRemoteAlbumService extends Mock implements RemoteAlbumService {}
|
||||
|
||||
class MockDriftMemoryService extends Mock implements DriftMemoryService {}
|
||||
|
||||
class MockDriftPeopleService extends Mock implements DriftPeopleService {}
|
||||
|
||||
class MockPlatformDeepLink extends Mock implements PlatformDeepLink {}
|
||||
|
||||
class MockWidgetRef extends Mock implements WidgetRef {}
|
||||
|
||||
class MockAssetViewerStateNotifier extends Mock implements AssetViewerStateNotifier {}
|
||||
|
||||
const _assetId = 'aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb';
|
||||
const _albumId = 'cccccccc-4444-5555-6666-dddddddddddd';
|
||||
|
||||
final _asset = RemoteAsset(
|
||||
id: _assetId,
|
||||
name: 'photo.jpg',
|
||||
ownerId: 'user-1',
|
||||
checksum: 'checksum-1',
|
||||
type: AssetType.image,
|
||||
createdAt: DateTime(2026, 6, 12),
|
||||
updatedAt: DateTime(2026, 6, 12),
|
||||
isEdited: false,
|
||||
);
|
||||
|
||||
final _album = RemoteAlbum(
|
||||
id: _albumId,
|
||||
name: 'Shared Album',
|
||||
ownerId: 'user-1',
|
||||
description: '',
|
||||
createdAt: DateTime(2026, 6, 12),
|
||||
updatedAt: DateTime(2026, 6, 12),
|
||||
isActivityEnabled: true,
|
||||
isShared: true,
|
||||
order: AlbumAssetOrder.asc,
|
||||
assetCount: 1,
|
||||
ownerName: 'Owner',
|
||||
);
|
||||
|
||||
void main() {
|
||||
late MockTimelineFactory timelineFactory;
|
||||
late MockAssetService assetService;
|
||||
late MockRemoteAlbumService remoteAlbumService;
|
||||
late MockWidgetRef ref;
|
||||
late List<TimelineService> createdTimelineServices;
|
||||
late DeepLinkService sut;
|
||||
|
||||
setUp(() {
|
||||
timelineFactory = MockTimelineFactory();
|
||||
assetService = MockAssetService();
|
||||
remoteAlbumService = MockRemoteAlbumService();
|
||||
ref = MockWidgetRef();
|
||||
createdTimelineServices = [];
|
||||
|
||||
when(() => timelineFactory.fromAssets(any(), TimelineOrigin.deepLink)).thenAnswer((invocation) {
|
||||
final assets = List<BaseAsset>.from(invocation.positionalArguments[0] as List<BaseAsset>);
|
||||
final timelineService = TimelineService((
|
||||
assetSource: (index, count) async => assets.skip(index).take(count).toList(),
|
||||
bucketSource: () => Stream.value([Bucket(assetCount: assets.length)]),
|
||||
origin: TimelineOrigin.deepLink,
|
||||
));
|
||||
createdTimelineServices.add(timelineService);
|
||||
return timelineService;
|
||||
});
|
||||
|
||||
when(() => ref.read(assetViewerProvider.notifier)).thenReturn(MockAssetViewerStateNotifier());
|
||||
|
||||
sut = DeepLinkService(
|
||||
timelineFactory,
|
||||
assetService,
|
||||
remoteAlbumService,
|
||||
MockDriftMemoryService(),
|
||||
MockDriftPeopleService(),
|
||||
null,
|
||||
);
|
||||
|
||||
addTearDown(() async {
|
||||
for (final timelineService in createdTimelineServices) {
|
||||
await timelineService.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
PlatformDeepLink link(String path) {
|
||||
final deepLink = MockPlatformDeepLink();
|
||||
when(() => deepLink.uri).thenReturn(Uri.parse('https://my.immich.app$path'));
|
||||
return deepLink;
|
||||
}
|
||||
|
||||
test('album photo link carries the album into the viewer route', () async {
|
||||
when(() => assetService.getRemoteAsset(_assetId)).thenAnswer((_) async => _asset);
|
||||
when(() => remoteAlbumService.get(_albumId)).thenAnswer((_) async => _album);
|
||||
|
||||
final route = await sut.handleMyImmichApp(link('/albums/$_albumId/photos/$_assetId'), ref);
|
||||
|
||||
expect(route, isA<AssetViewerRoute>());
|
||||
expect((route!.args as AssetViewerRouteArgs).currentAlbum, _album);
|
||||
});
|
||||
|
||||
test('still opens the viewer when the album cannot be resolved', () async {
|
||||
when(() => assetService.getRemoteAsset(_assetId)).thenAnswer((_) async => _asset);
|
||||
when(() => remoteAlbumService.get(_albumId)).thenAnswer((_) async => null);
|
||||
|
||||
final route = await sut.handleMyImmichApp(link('/albums/$_albumId/photos/$_assetId'), ref);
|
||||
|
||||
expect(route, isA<AssetViewerRoute>());
|
||||
expect((route!.args as AssetViewerRouteArgs).currentAlbum, isNull);
|
||||
});
|
||||
|
||||
test('plain photo link has no album', () async {
|
||||
when(() => assetService.getRemoteAsset(_assetId)).thenAnswer((_) async => _asset);
|
||||
|
||||
final route = await sut.handleMyImmichApp(link('/photos/$_assetId'), ref);
|
||||
|
||||
expect(route, isA<AssetViewerRoute>());
|
||||
expect((route!.args as AssetViewerRouteArgs).currentAlbum, isNull);
|
||||
verifyNever(() => remoteAlbumService.get(any()));
|
||||
});
|
||||
}
|
||||
@@ -261,10 +261,6 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if show.brokenAsset}
|
||||
<BrokenAsset class="absolute size-full text-xl" />
|
||||
{/if}
|
||||
|
||||
{#if show.preview}
|
||||
<ImageLayer
|
||||
{adaptiveImageLoader}
|
||||
@@ -290,6 +286,10 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if show.brokenAsset}
|
||||
<BrokenAsset class="absolute inset-0 z-10 size-full text-xl" />
|
||||
{/if}
|
||||
|
||||
{#if overlays}
|
||||
<div class="pointer-events-none absolute inset-0">
|
||||
{@render overlays()}
|
||||
|
||||
@@ -69,7 +69,7 @@ export enum OpenQueryParam {
|
||||
PURCHASE_SETTINGS = 'user-purchase-settings',
|
||||
}
|
||||
|
||||
export const maximumLengthSearchPeople = 1000;
|
||||
export const maximumLengthSearchPeople = 100;
|
||||
|
||||
// time to load the map before displaying the loading spinner
|
||||
export const timeToLoadTheMap: number = 100;
|
||||
|
||||
@@ -44,7 +44,7 @@ class AssetViewerManager extends BaseEventManager<Events> {
|
||||
imageLoaderStatus = $state<ImageLoaderStatus | undefined>();
|
||||
#isImageLoading = $derived.by(() => {
|
||||
const quality = this.imageLoaderStatus?.quality;
|
||||
if (!quality) {
|
||||
if (!quality || this.imageLoaderStatus?.hasError) {
|
||||
return false;
|
||||
}
|
||||
const previewOrOriginalReady = quality.preview === 'success' || quality.original === 'success';
|
||||
|
||||
Reference in New Issue
Block a user