feat(mobile): fallback to computed checksum for timeline match

- hash local asset on-demand when checksum missing
- search main timeline by localId or checksum before standalone viewer
- persist computed hash into local_asset_entity
This commit is contained in:
Peter Ombodi
2026-02-06 18:09:07 +02:00
parent bda0ceb2e2
commit bb803f13da
@@ -1,15 +1,18 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.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/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/models/view_intent/view_intent_attachment.model.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
@@ -21,6 +24,7 @@ final viewIntentHandlerProvider = Provider<ViewIntentHandler>(
ref.read(viewIntentServiceProvider),
ref.watch(appRouterProvider),
ref.read(localAssetRepository),
ref.read(nativeSyncApiProvider),
ref.read(timelineFactoryProvider),
),
);
@@ -30,6 +34,7 @@ class ViewIntentHandler {
final ViewIntentService _viewIntentService;
final AppRouter _router;
final DriftLocalAssetRepository _localAssetRepository;
final NativeSyncApi _nativeSyncApi;
final TimelineFactory _timelineFactory;
const ViewIntentHandler(
@@ -37,6 +42,7 @@ class ViewIntentHandler {
this._viewIntentService,
this._router,
this._localAssetRepository,
this._nativeSyncApi,
this._timelineFactory,
);
@@ -63,7 +69,19 @@ class ViewIntentHandler {
if (localAssetId != null) {
final localAsset = await _localAssetRepository.getById(localAssetId);
if (localAsset != null) {
_openAssetViewer(localAsset);
var checksum = localAsset.checksum;
if (checksum == null) {
checksum = await _computeChecksum(localAssetId);
if (checksum != null) {
await _localAssetRepository.updateHashes({localAssetId: checksum});
}
}
//todo clarify logic for assets not presented into MainTimeline (locked folder, deleted etc)
final timelineMatch = await _openFromMainTimeline(localAssetId, checksum: checksum);
if (timelineMatch) {
return;
}
_openAssetViewer(localAsset, _timelineFactory.fromAssets([localAsset], TimelineOrigin.deepLink), 0);
return;
}
}
@@ -71,7 +89,44 @@ class ViewIntentHandler {
await _router.push(ExternalMediaViewerRoute(attachment: attachment));
}
void _openAssetViewer(LocalAsset asset) {
Future<bool> _openFromMainTimeline(String localAssetId, {String? checksum}) async {
final timelineService = _ref.read(timelineServiceProvider);
if (timelineService.totalAssets == 0) {
try {
await timelineService.watchBuckets().first.timeout(const Duration(seconds: 2));
} catch (_) {
// Ignore and fallback.
}
}
final totalAssets = timelineService.totalAssets;
if (totalAssets == 0) {
return false;
}
final batchSize = kTimelineAssetLoadBatchSize;
for (var offset = 0; offset < totalAssets; offset += batchSize) {
final count = (offset + batchSize > totalAssets) ? totalAssets - offset : batchSize;
final assets = await timelineService.loadAssets(offset, count);
final indexInBatch = assets.indexWhere((asset) {
if (asset.localId == localAssetId) {
return true;
}
if (checksum != null && asset.checksum == checksum) {
return true;
}
return false;
});
if (indexInBatch >= 0) {
final asset = assets[indexInBatch];
_openAssetViewer(asset, timelineService, offset + indexInBatch);
return true;
}
}
return false;
}
void _openAssetViewer(BaseAsset asset, TimelineService timelineService, int initialIndex) {
_ref.read(assetViewerProvider.notifier).reset();
_ref.read(assetViewerProvider.notifier).setAsset(asset);
_ref.read(currentAssetNotifier.notifier).setAsset(asset);
@@ -83,11 +138,18 @@ class ViewIntentHandler {
_ref.read(assetViewerProvider.notifier).setControls(false);
}
_router.push(
AssetViewerRoute(
initialIndex: 0,
timelineService: _timelineFactory.fromAssets([asset], TimelineOrigin.deepLink),
),
);
_router.push(AssetViewerRoute(initialIndex: initialIndex, timelineService: timelineService));
}
Future<String?> _computeChecksum(String localAssetId) async {
try {
final hashResults = await _nativeSyncApi.hashAssets([localAssetId]);
if (hashResults.isEmpty) {
return null;
}
return hashResults.first.hash;
} catch (_) {
return null;
}
}
}