fix(mobile): ensure current asset is set in asset viewer (#21504)

This commit is contained in:
Mert 2025-09-02 11:03:44 -04:00 committed by GitHub
parent f06b054087
commit 873f7921da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 64 additions and 34 deletions

View File

@ -176,13 +176,13 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
final isColdStart = currentRouteName == null || currentRouteName == SplashScreenRoute.name;
if (deepLink.uri.scheme == "immich") {
final proposedRoute = await deepLinkHandler.handleScheme(deepLink, isColdStart);
final proposedRoute = await deepLinkHandler.handleScheme(deepLink, ref, isColdStart);
return proposedRoute;
}
if (deepLink.uri.host == "my.immich.app") {
final proposedRoute = await deepLinkHandler.handleMyImmichApp(deepLink, isColdStart);
final proposedRoute = await deepLinkHandler.handleMyImmichApp(deepLink, ref, isColdStart);
return proposedRoute;
}

View File

@ -59,6 +59,18 @@ class AssetViewer extends ConsumerStatefulWidget {
@override
ConsumerState createState() => _AssetViewerState();
static void setAsset(WidgetRef ref, BaseAsset asset) {
// Always holds the current asset from the timeline
ref.read(assetViewerProvider.notifier).setAsset(asset);
// The currentAssetNotifier actually holds the current asset that is displayed
// which could be stack children as well
ref.read(currentAssetNotifier.notifier).setAsset(asset);
if (asset.isVideo || asset.isMotionPhoto) {
ref.read(videoPlaybackValueProvider.notifier).reset();
ref.read(videoPlayerControlsProvider.notifier).pause();
}
}
}
const double _kBottomSheetMinimumExtent = 0.4;
@ -99,13 +111,12 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
@override
void initState() {
super.initState();
assert(ref.read(currentAssetNotifier) != null, "Current asset should not be null when opening the AssetViewer");
pageController = PageController(initialPage: widget.initialIndex);
platform = widget.platform ?? const LocalPlatform();
totalAssets = ref.read(timelineServiceProvider).totalAssets;
bottomSheetController = DraggableScrollableController();
WidgetsBinding.instance.addPostFrameCallback((_) {
_onAssetChanged(widget.initialIndex);
});
WidgetsBinding.instance.addPostFrameCallback(_onAssetInit);
reloadSubscription = EventStream.shared.listen(_onEvent);
heroOffset = widget.heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0;
}
@ -143,26 +154,9 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
return provider.resolve(ImageConfiguration.empty)..addListener(_dummyListener);
}
void _onAssetChanged(int index) async {
// Validate index bounds and try to get asset, loading buffer if needed
void _precacheAssets(int index) {
final timelineService = ref.read(timelineServiceProvider);
final asset = await timelineService.getAssetAsync(index);
if (asset == null) {
return;
}
// Always holds the current asset from the timeline
ref.read(assetViewerProvider.notifier).setAsset(asset);
// The currentAssetNotifier actually holds the current asset that is displayed
// which could be stack children as well
ref.read(currentAssetNotifier.notifier).setAsset(asset);
if (asset.isVideo || asset.isMotionPhoto) {
ref.read(videoPlaybackValueProvider.notifier).reset();
ref.read(videoPlayerControlsProvider.notifier).pause();
}
unawaited(ref.read(timelineServiceProvider).preCacheAssets(index));
unawaited(timelineService.preCacheAssets(index));
_cancelTimers();
// This will trigger the pre-caching of adjacent assets ensuring
// that they are ready when the user navigates to them.
@ -181,12 +175,29 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
_nextPreCacheStream = nextAsset != null ? _precacheImage(nextAsset) : null;
});
_delayedOperations.add(timer);
_handleCasting(asset);
}
void _handleCasting(BaseAsset asset) {
void _onAssetInit(Duration _) {
_precacheAssets(widget.initialIndex);
_handleCasting();
}
void _onAssetChanged(int index) async {
final timelineService = ref.read(timelineServiceProvider);
final asset = await timelineService.getAssetAsync(index);
if (asset == null) {
return;
}
AssetViewer.setAsset(ref, asset);
_precacheAssets(index);
_handleCasting();
}
void _handleCasting() {
if (!ref.read(castProvider).isCasting) return;
final asset = ref.read(currentAssetNotifier);
if (asset == null) return;
// hide any casting snackbars if they exist
context.scaffoldMessenger.hideCurrentSnackBar();
@ -597,7 +608,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
if (asset == null) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
_handleCasting(asset);
_handleCasting();
});
});

View File

@ -75,14 +75,23 @@ class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
}
void setAsset(BaseAsset? asset) {
if (asset == state.currentAsset) {
return;
}
state = state.copyWith(currentAsset: asset, stackIndex: 0);
}
void setOpacity(int opacity) {
if (opacity == state.backgroundOpacity) {
return;
}
state = state.copyWith(backgroundOpacity: opacity, showingControls: opacity == 255 ? true : state.showingControls);
}
void setBottomSheet(bool showing) {
if (showing == state.showingBottomSheet) {
return;
}
state = state.copyWith(showingBottomSheet: showing, showingControls: showing ? true : state.showingControls);
if (showing) {
ref.read(videoPlayerControlsProvider.notifier).pause();
@ -90,6 +99,9 @@ class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
}
void setControls(bool isShowing) {
if (isShowing == state.showingControls) {
return;
}
state = state.copyWith(showingControls: isShowing);
}
@ -98,6 +110,9 @@ class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
}
void setStackIndex(int index) {
if (index == state.stackIndex) {
return;
}
state = state.copyWith(stackIndex: index);
}
}

View File

@ -5,6 +5,7 @@ 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/timeline.service.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail_tile.widget.dart';
import 'package:immich_mobile/presentation/widgets/timeline/fixed/row.dart';
import 'package:immich_mobile/presentation/widgets/timeline/header.widget.dart';
@ -155,6 +156,7 @@ class _AssetTileWidget extends ConsumerWidget {
} else {
await ref.read(timelineServiceProvider).loadAssets(assetIndex, 1);
ref.read(isPlayingMotionVideoProvider.notifier).playing = false;
AssetViewer.setAsset(ref, asset);
ctx.pushRoute(
AssetViewerRoute(
initialIndex: assetIndex,

View File

@ -1,9 +1,11 @@
import 'package:auto_route/auto_route.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/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/asset.provider.dart' as beta_asset_provider;
@ -16,7 +18,6 @@ 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';
import 'package:riverpod_annotation/riverpod_annotation.dart';
final deepLinkServiceProvider = Provider(
(ref) => DeepLinkService(
@ -71,14 +72,14 @@ class DeepLinkService {
]);
}
Future<DeepLink> handleScheme(PlatformDeepLink link, bool isColdStart) async {
Future<DeepLink> 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<dynamic>? deepLinkRoute = switch (intent) {
"memory" => await _buildMemoryDeepLink(queryParams['id'] ?? ''),
"asset" => await _buildAssetDeepLink(queryParams['id'] ?? ''),
"asset" => await _buildAssetDeepLink(queryParams['id'] ?? '', ref),
"album" => await _buildAlbumDeepLink(queryParams['id'] ?? ''),
_ => null,
};
@ -95,7 +96,7 @@ class DeepLinkService {
return _handleColdStart(deepLinkRoute, isColdStart);
}
Future<DeepLink> handleMyImmichApp(PlatformDeepLink link, bool isColdStart) async {
Future<DeepLink> 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}';
@ -105,7 +106,7 @@ class DeepLinkService {
PageRouteInfo<dynamic>? deepLinkRoute;
if (assetRegex.hasMatch(path)) {
final assetId = assetRegex.firstMatch(path)?.group(1) ?? '';
deepLinkRoute = await _buildAssetDeepLink(assetId);
deepLinkRoute = await _buildAssetDeepLink(assetId, ref);
} else if (albumRegex.hasMatch(path)) {
final albumId = albumRegex.firstMatch(path)?.group(1) ?? '';
deepLinkRoute = await _buildAlbumDeepLink(albumId);
@ -141,13 +142,14 @@ class DeepLinkService {
}
}
Future<PageRouteInfo?> _buildAssetDeepLink(String assetId) async {
Future<PageRouteInfo?> _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]));
} else {
// TODO: Remove this when beta is default