Files
immich/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart
T
9d4a6614b1 feat(mobile): Android. Immich as a gallery / image viewer app (#26109)
* feat(mobile): handle Android ACTION_VIEW intent
- add ViewIntent Pigeon API and generated bindings
- implement Android ViewIntentPlugin + iOS no-op host
- route ExternalMediaViewer by ViewIntentAttachment
- buffer pending view intents and flush on user ready/resume

* 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

* fix(mobile): proper handling is user authenticated

* feat(mobile): open ACTION_VIEW fallback in AssetViewer
drop ExternalMediaViewer route

* feat(mobile): add logger

* test(mobile): add unit tests for view intent pending/flush flow

* fix(mobile): fix format

* fix(mobile): remove redundant iOS code
update code related to LocalAsset model and asset viewer

* refactor(mobile): simplify view intent flow and support file-backed ACTION_VIEW assets
remove redundant view intent model/repository layer
handle transient ACTION_VIEW files in viewer/upload flow
clean up managed temp files for fallback assets

* refactor(mobile): extract MediaStore utils and resolve view intents via merged assets

* refactor(mobile): move deferred view intents into providers, split view-intent providers, and clean up ACTION_VIEW handling

* refactor(mobile): resolve merge conflicts
use NativeSyncApi for hash files instead method from removed BackgroundServicePlugin.kt

* style(mobile): format files

* style(mobile): format files #2

* refactor(mobile): lazily materialize view-intent files and clean up temp-file handling

* fix(mobile): flush pending view intents after login navigation

* refactor(mobile): split view intent handler by platform and trigger it from app events

* refactor(mobile): move view intent handling behind platform-specific factories

* refactor(mobile): simplify code

* fix(mobile): hand off deep-link viewer to main timeline after upload
Add MainTimelineHandoffCoordinator to switch the asset viewer to the main timeline once a view-intent asset is uploaded and becomes available, and guard viewer reload/navigation transitions to avoid race conditions and crashes.

* refactor(mobile): use remote asset ids for view intent handoff and simplify resolver

* refactor(mobile): resolve merge conflicts

* style(mobile): reformat code

* style(mobile): reformat code #2

* fix(mobile): stabilize Android view intent asset resolution and fallback viewer

* refactor(mobile): share AssetViewer pre-navigation state preparation

* fix(mobile): wait for main timeline before deferred view intent handoff

* refactor(mobile): decouple view intent asset resolver from providers

* fix(mobile): avoid double pop when canceling upload dialog

* fix(mobile): resolve view intent MIME type with fallbacks

* docs(mobile): clarify view intent fallback asset TODO

* fix(mobile): resolve merge conflicts

* cleanup

* lint

---------

Co-authored-by: Peter Ombodi <peter.ombodi@gmail.com>
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2026-06-03 12:05:52 -05:00

102 lines
3.2 KiB
Dart

import 'dart:io';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/foreground_upload.service.dart';
import 'package:immich_mobile/services/share_intent_service.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
final shareIntentUploadProvider = StateNotifierProvider<ShareIntentUploadStateNotifier, List<ShareIntentAttachment>>(
((ref) => ShareIntentUploadStateNotifier(
ref.watch(appRouterProvider),
ref.read(foregroundUploadServiceProvider),
ref.read(shareIntentServiceProvider),
)),
);
class ShareIntentUploadStateNotifier extends StateNotifier<List<ShareIntentAttachment>> {
final AppRouter router;
final ForegroundUploadService _foregroundUploadService;
final ShareIntentService _shareIntentService;
final Logger _logger = Logger('ShareIntentUploadStateNotifier');
ShareIntentUploadStateNotifier(this.router, this._foregroundUploadService, this._shareIntentService) : super([]);
void init() {
_shareIntentService.onSharedMedia = onSharedMedia;
_shareIntentService.init();
}
void onSharedMedia(List<ShareIntentAttachment> attachments) {
router.removeWhere((route) => route.name == "ShareIntentRoute");
clearAttachments();
addAttachments(attachments);
router.push(ShareIntentRoute(attachments: attachments));
}
void addAttachments(List<ShareIntentAttachment> attachments) {
if (attachments.isEmpty) {
return;
}
state = [...state, ...attachments];
}
void removeAttachment(ShareIntentAttachment attachment) {
final updatedState = state.where((element) => element != attachment).toList();
if (updatedState.length != state.length) {
state = updatedState;
}
}
void clearAttachments() {
if (state.isEmpty) {
return;
}
state = [];
}
Future<void> uploadAll(List<File> files) async {
for (final file in files) {
final fileId = p.hash(file.path).toString();
_updateStatus(fileId, UploadStatus.running);
}
await _foregroundUploadService.uploadShareIntent(
files,
onProgress: (fileId, bytes, totalBytes) {
final progress = totalBytes > 0 ? bytes / totalBytes : 0.0;
_updateProgress(fileId, progress);
},
onSuccess: (fileId, _) {
_updateStatus(fileId, UploadStatus.complete, progress: 1.0);
},
onError: (fileId, errorMessage) {
_logger.warning("Upload failed for file: $fileId, error: $errorMessage");
_updateStatus(fileId, UploadStatus.failed);
},
);
}
void _updateStatus(String fileId, UploadStatus status, {double? progress}) {
final id = int.parse(fileId);
state = [
for (final attachment in state)
if (attachment.id == id)
attachment.copyWith(status: status, uploadProgress: progress ?? attachment.uploadProgress)
else
attachment,
];
}
void _updateProgress(String fileId, double progress) {
final id = int.parse(fileId);
state = [
for (final attachment in state)
if (attachment.id == id) attachment.copyWith(uploadProgress: progress) else attachment,
];
}
}