Skip to content

Commit 873f792

Browse files
authored
fix(mobile): ensure current asset is set in asset viewer (#21504)
1 parent f06b054 commit 873f792

File tree

5 files changed

+63
-33
lines changed

5 files changed

+63
-33
lines changed

mobile/lib/main.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,13 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
176176
final isColdStart = currentRouteName == null || currentRouteName == SplashScreenRoute.name;
177177

178178
if (deepLink.uri.scheme == "immich") {
179-
final proposedRoute = await deepLinkHandler.handleScheme(deepLink, isColdStart);
179+
final proposedRoute = await deepLinkHandler.handleScheme(deepLink, ref, isColdStart);
180180

181181
return proposedRoute;
182182
}
183183

184184
if (deepLink.uri.host == "my.immich.app") {
185-
final proposedRoute = await deepLinkHandler.handleMyImmichApp(deepLink, isColdStart);
185+
final proposedRoute = await deepLinkHandler.handleMyImmichApp(deepLink, ref, isColdStart);
186186

187187
return proposedRoute;
188188
}

mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@ class AssetViewer extends ConsumerStatefulWidget {
5959

6060
@override
6161
ConsumerState createState() => _AssetViewerState();
62+
63+
static void setAsset(WidgetRef ref, BaseAsset asset) {
64+
// Always holds the current asset from the timeline
65+
ref.read(assetViewerProvider.notifier).setAsset(asset);
66+
// The currentAssetNotifier actually holds the current asset that is displayed
67+
// which could be stack children as well
68+
ref.read(currentAssetNotifier.notifier).setAsset(asset);
69+
if (asset.isVideo || asset.isMotionPhoto) {
70+
ref.read(videoPlaybackValueProvider.notifier).reset();
71+
ref.read(videoPlayerControlsProvider.notifier).pause();
72+
}
73+
}
6274
}
6375

6476
const double _kBottomSheetMinimumExtent = 0.4;
@@ -99,13 +111,12 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
99111
@override
100112
void initState() {
101113
super.initState();
114+
assert(ref.read(currentAssetNotifier) != null, "Current asset should not be null when opening the AssetViewer");
102115
pageController = PageController(initialPage: widget.initialIndex);
103116
platform = widget.platform ?? const LocalPlatform();
104117
totalAssets = ref.read(timelineServiceProvider).totalAssets;
105118
bottomSheetController = DraggableScrollableController();
106-
WidgetsBinding.instance.addPostFrameCallback((_) {
107-
_onAssetChanged(widget.initialIndex);
108-
});
119+
WidgetsBinding.instance.addPostFrameCallback(_onAssetInit);
109120
reloadSubscription = EventStream.shared.listen(_onEvent);
110121
heroOffset = widget.heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0;
111122
}
@@ -143,26 +154,9 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
143154
return provider.resolve(ImageConfiguration.empty)..addListener(_dummyListener);
144155
}
145156

146-
void _onAssetChanged(int index) async {
147-
// Validate index bounds and try to get asset, loading buffer if needed
157+
void _precacheAssets(int index) {
148158
final timelineService = ref.read(timelineServiceProvider);
149-
final asset = await timelineService.getAssetAsync(index);
150-
151-
if (asset == null) {
152-
return;
153-
}
154-
155-
// Always holds the current asset from the timeline
156-
ref.read(assetViewerProvider.notifier).setAsset(asset);
157-
// The currentAssetNotifier actually holds the current asset that is displayed
158-
// which could be stack children as well
159-
ref.read(currentAssetNotifier.notifier).setAsset(asset);
160-
if (asset.isVideo || asset.isMotionPhoto) {
161-
ref.read(videoPlaybackValueProvider.notifier).reset();
162-
ref.read(videoPlayerControlsProvider.notifier).pause();
163-
}
164-
165-
unawaited(ref.read(timelineServiceProvider).preCacheAssets(index));
159+
unawaited(timelineService.preCacheAssets(index));
166160
_cancelTimers();
167161
// This will trigger the pre-caching of adjacent assets ensuring
168162
// that they are ready when the user navigates to them.
@@ -181,12 +175,29 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
181175
_nextPreCacheStream = nextAsset != null ? _precacheImage(nextAsset) : null;
182176
});
183177
_delayedOperations.add(timer);
178+
}
179+
180+
void _onAssetInit(Duration _) {
181+
_precacheAssets(widget.initialIndex);
182+
_handleCasting();
183+
}
184+
185+
void _onAssetChanged(int index) async {
186+
final timelineService = ref.read(timelineServiceProvider);
187+
final asset = await timelineService.getAssetAsync(index);
188+
if (asset == null) {
189+
return;
190+
}
184191

185-
_handleCasting(asset);
192+
AssetViewer.setAsset(ref, asset);
193+
_precacheAssets(index);
194+
_handleCasting();
186195
}
187196

188-
void _handleCasting(BaseAsset asset) {
197+
void _handleCasting() {
189198
if (!ref.read(castProvider).isCasting) return;
199+
final asset = ref.read(currentAssetNotifier);
200+
if (asset == null) return;
190201

191202
// hide any casting snackbars if they exist
192203
context.scaffoldMessenger.hideCurrentSnackBar();
@@ -597,7 +608,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
597608
if (asset == null) return;
598609

599610
WidgetsBinding.instance.addPostFrameCallback((_) {
600-
_handleCasting(asset);
611+
_handleCasting();
601612
});
602613
});
603614

mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,21 +75,33 @@ class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
7575
}
7676

7777
void setAsset(BaseAsset? asset) {
78+
if (asset == state.currentAsset) {
79+
return;
80+
}
7881
state = state.copyWith(currentAsset: asset, stackIndex: 0);
7982
}
8083

8184
void setOpacity(int opacity) {
85+
if (opacity == state.backgroundOpacity) {
86+
return;
87+
}
8288
state = state.copyWith(backgroundOpacity: opacity, showingControls: opacity == 255 ? true : state.showingControls);
8389
}
8490

8591
void setBottomSheet(bool showing) {
92+
if (showing == state.showingBottomSheet) {
93+
return;
94+
}
8695
state = state.copyWith(showingBottomSheet: showing, showingControls: showing ? true : state.showingControls);
8796
if (showing) {
8897
ref.read(videoPlayerControlsProvider.notifier).pause();
8998
}
9099
}
91100

92101
void setControls(bool isShowing) {
102+
if (isShowing == state.showingControls) {
103+
return;
104+
}
93105
state = state.copyWith(showingControls: isShowing);
94106
}
95107

@@ -98,6 +110,9 @@ class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
98110
}
99111

100112
void setStackIndex(int index) {
113+
if (index == state.stackIndex) {
114+
return;
115+
}
101116
state = state.copyWith(stackIndex: index);
102117
}
103118
}

mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
55
import 'package:hooks_riverpod/hooks_riverpod.dart';
66
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
77
import 'package:immich_mobile/domain/services/timeline.service.dart';
8+
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart';
89
import 'package:immich_mobile/presentation/widgets/images/thumbnail_tile.widget.dart';
910
import 'package:immich_mobile/presentation/widgets/timeline/fixed/row.dart';
1011
import 'package:immich_mobile/presentation/widgets/timeline/header.widget.dart';
@@ -155,6 +156,7 @@ class _AssetTileWidget extends ConsumerWidget {
155156
} else {
156157
await ref.read(timelineServiceProvider).loadAssets(assetIndex, 1);
157158
ref.read(isPlayingMotionVideoProvider.notifier).playing = false;
159+
AssetViewer.setAsset(ref, asset);
158160
ctx.pushRoute(
159161
AssetViewerRoute(
160162
initialIndex: assetIndex,

mobile/lib/services/deep_link.service.dart

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import 'package:auto_route/auto_route.dart';
2+
import 'package:hooks_riverpod/hooks_riverpod.dart';
23
import 'package:immich_mobile/domain/services/asset.service.dart' as beta_asset_service;
34
import 'package:immich_mobile/domain/services/memory.service.dart';
45
import 'package:immich_mobile/domain/services/remote_album.service.dart';
56
import 'package:immich_mobile/domain/services/timeline.service.dart';
67
import 'package:immich_mobile/entities/store.entity.dart';
8+
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart';
79
import 'package:immich_mobile/providers/album/current_album.provider.dart';
810
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
911
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart' as beta_asset_provider;
@@ -16,7 +18,6 @@ import 'package:immich_mobile/services/album.service.dart';
1618
import 'package:immich_mobile/services/asset.service.dart';
1719
import 'package:immich_mobile/services/memory.service.dart';
1820
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
19-
import 'package:riverpod_annotation/riverpod_annotation.dart';
2021

2122
final deepLinkServiceProvider = Provider(
2223
(ref) => DeepLinkService(
@@ -71,14 +72,14 @@ class DeepLinkService {
7172
]);
7273
}
7374

74-
Future<DeepLink> handleScheme(PlatformDeepLink link, bool isColdStart) async {
75+
Future<DeepLink> handleScheme(PlatformDeepLink link, WidgetRef ref, bool isColdStart) async {
7576
// get everything after the scheme, since Uri cannot parse path
7677
final intent = link.uri.host;
7778
final queryParams = link.uri.queryParameters;
7879

7980
PageRouteInfo<dynamic>? deepLinkRoute = switch (intent) {
8081
"memory" => await _buildMemoryDeepLink(queryParams['id'] ?? ''),
81-
"asset" => await _buildAssetDeepLink(queryParams['id'] ?? ''),
82+
"asset" => await _buildAssetDeepLink(queryParams['id'] ?? '', ref),
8283
"album" => await _buildAlbumDeepLink(queryParams['id'] ?? ''),
8384
_ => null,
8485
};
@@ -95,7 +96,7 @@ class DeepLinkService {
9596
return _handleColdStart(deepLinkRoute, isColdStart);
9697
}
9798

98-
Future<DeepLink> handleMyImmichApp(PlatformDeepLink link, bool isColdStart) async {
99+
Future<DeepLink> handleMyImmichApp(PlatformDeepLink link, WidgetRef ref, bool isColdStart) async {
99100
final path = link.uri.path;
100101

101102
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}';
@@ -105,7 +106,7 @@ class DeepLinkService {
105106
PageRouteInfo<dynamic>? deepLinkRoute;
106107
if (assetRegex.hasMatch(path)) {
107108
final assetId = assetRegex.firstMatch(path)?.group(1) ?? '';
108-
deepLinkRoute = await _buildAssetDeepLink(assetId);
109+
deepLinkRoute = await _buildAssetDeepLink(assetId, ref);
109110
} else if (albumRegex.hasMatch(path)) {
110111
final albumId = albumRegex.firstMatch(path)?.group(1) ?? '';
111112
deepLinkRoute = await _buildAlbumDeepLink(albumId);
@@ -141,13 +142,14 @@ class DeepLinkService {
141142
}
142143
}
143144

144-
Future<PageRouteInfo?> _buildAssetDeepLink(String assetId) async {
145+
Future<PageRouteInfo?> _buildAssetDeepLink(String assetId, WidgetRef ref) async {
145146
if (Store.isBetaTimelineEnabled) {
146147
final asset = await _betaAssetService.getRemoteAsset(assetId);
147148
if (asset == null) {
148149
return null;
149150
}
150151

152+
AssetViewer.setAsset(ref, asset);
151153
return AssetViewerRoute(initialIndex: 0, timelineService: _betaTimelineFactory.fromAssets([asset]));
152154
} else {
153155
// TODO: Remove this when beta is default

0 commit comments

Comments
 (0)