mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
171 lines
6.4 KiB
Dart
171 lines
6.4 KiB
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/asset.service.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_payload.extension.dart';
|
|
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
|
import 'package:immich_mobile/platform/view_intent_api.g.dart';
|
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
|
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
|
import 'package:logging/logging.dart';
|
|
|
|
class ViewIntentResolution {
|
|
final BaseAsset asset;
|
|
final TimelineService timelineService;
|
|
|
|
final String? viewIntentFilePath;
|
|
|
|
const ViewIntentResolution({required this.asset, required this.timelineService, this.viewIntentFilePath});
|
|
}
|
|
|
|
final viewIntentAssetResolverProvider = Provider<ViewIntentAssetResolver>(
|
|
(ref) => ViewIntentAssetResolver(
|
|
localAssetRepository: ref.read(localAssetRepository),
|
|
assetService: ref.read(assetServiceProvider),
|
|
nativeSyncApi: ref.read(nativeSyncApiProvider),
|
|
timelineFactory: ref.read(timelineFactoryProvider),
|
|
),
|
|
);
|
|
|
|
class ViewIntentAssetResolver {
|
|
final DriftLocalAssetRepository _localAssetRepository;
|
|
final AssetService _assetService;
|
|
final NativeSyncApi _nativeSyncApi;
|
|
final TimelineFactory _timelineFactory;
|
|
static final Logger _logger = Logger('ViewIntentAssetResolver');
|
|
|
|
const ViewIntentAssetResolver({
|
|
required this._localAssetRepository,
|
|
required this._assetService,
|
|
required this._nativeSyncApi,
|
|
required this._timelineFactory,
|
|
});
|
|
|
|
Future<ViewIntentResolution> resolve(ViewIntentPayload attachment) async {
|
|
final localAssetId = attachment.localAssetId;
|
|
final path = attachment.path;
|
|
_logger.fine('resolve start, localAssetId=$localAssetId, path=$path, mimeType=${attachment.mimeType}');
|
|
|
|
if (localAssetId == null && path == null) {
|
|
throw StateError('ViewIntent resolution requires either a localAssetId or a materialized file path.');
|
|
}
|
|
|
|
({LocalAsset? asset, String? checksum}) resolvedLocal = (asset: null, checksum: null);
|
|
if (localAssetId != null) {
|
|
resolvedLocal = await _resolveLocalAsset(localAssetId);
|
|
final remoteAsset = await _resolveRemoteAsset(
|
|
localAssetId,
|
|
remoteAssetId: resolvedLocal.asset?.remoteId,
|
|
checksum: resolvedLocal.checksum,
|
|
);
|
|
if (remoteAsset != null) {
|
|
return ViewIntentResolution(asset: remoteAsset, timelineService: _timelineFor(remoteAsset));
|
|
}
|
|
}
|
|
|
|
final asset = resolvedLocal.asset ?? _toTransientAsset(attachment, resolvedLocal.checksum);
|
|
|
|
return ViewIntentResolution(
|
|
asset: asset,
|
|
timelineService: _timelineFor(asset),
|
|
viewIntentFilePath: resolvedLocal.asset == null ? path : null,
|
|
);
|
|
}
|
|
|
|
TimelineService _timelineFor(BaseAsset asset) {
|
|
final origin = asset is RemoteAsset && asset.isTrashed ? TimelineOrigin.deepLinkTrash : TimelineOrigin.deepLink;
|
|
return _timelineFactory.fromAssets([asset], origin);
|
|
}
|
|
|
|
Future<({LocalAsset? asset, String? checksum})> _resolveLocalAsset(String localAssetId) async {
|
|
final localAsset = await _localAssetRepository.get(localAssetId);
|
|
final checksum = localAsset?.checksum ?? await _hashLocalAsset(localAssetId);
|
|
|
|
if (checksum == null || checksum == localAsset?.checksum) {
|
|
return (asset: localAsset, checksum: checksum);
|
|
}
|
|
|
|
if (localAsset != null) {
|
|
await _localAssetRepository.updateHashes({localAssetId: checksum});
|
|
final resolvedAsset = await _localAssetRepository.get(localAssetId);
|
|
return (asset: resolvedAsset ?? localAsset.copyWith(checksum: checksum), checksum: checksum);
|
|
}
|
|
|
|
return (asset: null, checksum: checksum);
|
|
}
|
|
|
|
Future<String?> _hashLocalAsset(String localAssetId) async {
|
|
try {
|
|
final hashResults = await _nativeSyncApi.hashAssets([localAssetId]);
|
|
if (hashResults.isEmpty) {
|
|
return null;
|
|
}
|
|
|
|
final result = hashResults.first;
|
|
if (result.error != null) {
|
|
_logger.warning('Failed to hash view intent local asset $localAssetId: ${result.error}');
|
|
return null;
|
|
}
|
|
return result.hash;
|
|
} catch (error, stackTrace) {
|
|
_logger.warning('Failed to hash view intent local asset $localAssetId', error, stackTrace);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<RemoteAsset?> _resolveRemoteAsset(
|
|
String localAssetId, {
|
|
required String? remoteAssetId,
|
|
required String? checksum,
|
|
}) async {
|
|
RemoteAsset? remoteAsset;
|
|
if (remoteAssetId != null) {
|
|
remoteAsset = await _assetService.getRemoteAsset(remoteAssetId);
|
|
if (remoteAsset != null) {
|
|
_logger.fine('resolve matched remote asset by id: $remoteAssetId, asset=$remoteAsset');
|
|
}
|
|
}
|
|
|
|
if (remoteAsset == null && checksum != null) {
|
|
final candidates = await _assetService.getAllRemoteAssetDebugByChecksum(checksum);
|
|
if (candidates.isNotEmpty) {
|
|
remoteAsset = ([...candidates]..sort(_compareRemoteAssetCandidates)).first;
|
|
_logger.fine('resolve matched remote asset by checksum: $checksum, asset=$remoteAsset');
|
|
}
|
|
}
|
|
|
|
if (remoteAsset == null) {
|
|
return null;
|
|
}
|
|
final asset = remoteAsset.copyWith(localId: localAssetId);
|
|
return asset;
|
|
}
|
|
|
|
static int _compareRemoteAssetCandidates(RemoteAsset first, RemoteAsset second) {
|
|
if (first.isTrashed != second.isTrashed) {
|
|
return first.isTrashed ? 1 : -1;
|
|
}
|
|
|
|
final firstDate = first.uploadedAt ?? first.createdAt;
|
|
final secondDate = second.uploadedAt ?? second.createdAt;
|
|
final date = secondDate.compareTo(firstDate);
|
|
return date != 0 ? date : first.id.compareTo(second.id);
|
|
}
|
|
|
|
LocalAsset _toTransientAsset(ViewIntentPayload attachment, String? checksum) {
|
|
final now = DateTime.now();
|
|
// A FileBackedAsset could model the path more explicitly, but would require broader changes to the asset hierarchy.
|
|
return LocalAsset(
|
|
id: attachment.localAssetId ?? '-${attachment.path!.hashCode.abs()}',
|
|
name: attachment.fileName,
|
|
checksum: checksum,
|
|
type: attachment.isVideo ? AssetType.video : AssetType.image,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
isEdited: false,
|
|
playbackStyle: attachment.playbackStyle,
|
|
);
|
|
}
|
|
}
|