mirror of
https://github.com/immich-app/immich.git
synced 2026-05-22 23:52:32 -04:00
refactor(mobile): split view intent handler by platform and trigger it from app events
This commit is contained in:
+26
-30
@@ -19,13 +19,13 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||
import 'package:immich_mobile/pages/common/splash_screen.page.dart';
|
||||
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
|
||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
||||
import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:immich_mobile/pages/common/splash_screen.page.dart';
|
||||
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
|
||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
||||
import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:immich_mobile/providers/locale_provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/theme.provider.dart';
|
||||
@@ -120,19 +120,16 @@ class ImmichApp extends ConsumerStatefulWidget {
|
||||
ImmichAppState createState() => ImmichAppState();
|
||||
}
|
||||
|
||||
class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserver {
|
||||
ProviderSubscription<bool>? _authSubscription;
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserver {
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
switch (state) {
|
||||
case AppLifecycleState.resumed:
|
||||
dPrint(() => "[APP STATE] resumed");
|
||||
ref.read(appStateProvider.notifier).handleAppResume();
|
||||
// Check for ACTION_VIEW intent when app resumes
|
||||
unawaited(ref.read(viewIntentHandlerProvider).checkForViewIntent());
|
||||
unawaited(ref.read(viewIntentHandlerProvider).flushPending());
|
||||
break;
|
||||
dPrint(() => "[APP STATE] resumed");
|
||||
ref.read(appStateProvider.notifier).handleAppResume();
|
||||
unawaited(ref.read(viewIntentHandlerProvider).onAppResumed());
|
||||
break;
|
||||
case AppLifecycleState.inactive:
|
||||
dPrint(() => "[APP STATE] inactive");
|
||||
ref.read(appStateProvider.notifier).handleAppInactivity();
|
||||
@@ -215,18 +212,17 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
||||
StaticTranslations.instance.backup_background_service_default_notification,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
ref.read(viewIntentHandlerProvider).init();
|
||||
ref.read(shareIntentUploadProvider.notifier).init();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_authSubscription?.close();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
ref.read(viewIntentHandlerProvider).init();
|
||||
ref.read(shareIntentUploadProvider.notifier).init();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void reassemble() {
|
||||
|
||||
@@ -1,105 +1,24 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
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/platform/view_intent_api.g.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/view_intent/view_intent_file_path.provider.dart';
|
||||
import 'package:immich_mobile/providers/view_intent/view_intent_pending.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/view_intent.service.dart';
|
||||
import 'package:immich_mobile/services/view_intent_asset_resolver.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:immich_mobile/providers/view_intent/view_intent_handler_android.dart';
|
||||
import 'package:immich_mobile/providers/view_intent/view_intent_handler_stub.dart';
|
||||
|
||||
final viewIntentHandlerProvider = Provider<ViewIntentHandler>(
|
||||
(ref) => ViewIntentHandler(
|
||||
ref,
|
||||
ref.read(viewIntentServiceProvider),
|
||||
ref.read(viewIntentAssetResolverProvider),
|
||||
ref.watch(appRouterProvider),
|
||||
),
|
||||
);
|
||||
abstract class ViewIntentHandler {
|
||||
void init();
|
||||
|
||||
class ViewIntentHandler {
|
||||
final Ref _ref;
|
||||
final ViewIntentService _viewIntentService;
|
||||
final ViewIntentAssetResolver _viewIntentAssetResolver;
|
||||
final AppRouter _router;
|
||||
static final Logger _logger = Logger('ViewIntentHandler');
|
||||
Future<void> onAppResumed();
|
||||
|
||||
const ViewIntentHandler(this._ref, this._viewIntentService, this._viewIntentAssetResolver, this._router);
|
||||
Future<void> onUserAuthenticated();
|
||||
|
||||
void init() {
|
||||
unawaited(checkForViewIntent());
|
||||
unawaited(
|
||||
Future(() async {
|
||||
await flushPending();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> checkForViewIntent() async {
|
||||
final attachment = await _viewIntentService.consumeViewIntent();
|
||||
if (attachment != null) {
|
||||
await handle(attachment);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_ref.read(viewIntentPendingProvider) == null) {
|
||||
await _viewIntentService.cleanupStaleTempFiles();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> flushPending() async {
|
||||
final pendingAttachment = _ref.read(viewIntentPendingProvider.notifier).takeIfFresh();
|
||||
_logger.info('flushPending, pendingAttachment:$pendingAttachment}');
|
||||
if (pendingAttachment != null) {
|
||||
await handle(pendingAttachment);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handle(ViewIntentPayload attachment) async {
|
||||
_logger.info(
|
||||
'handle attachment, mimeType:${attachment.mimeType}, localAssetId=${attachment.localAssetId}, path=${attachment.path}, isAuthenticated:${_ref.read(authProvider).isAuthenticated}',
|
||||
);
|
||||
|
||||
if (!_ref.read(authProvider).isAuthenticated) {
|
||||
_ref.read(viewIntentPendingProvider.notifier).defer(attachment);
|
||||
return;
|
||||
}
|
||||
|
||||
final resolvedAsset = await _viewIntentAssetResolver.resolve(attachment);
|
||||
_logger.fine('resolved view intent asset: ${resolvedAsset.asset}');
|
||||
_openAssetViewer(
|
||||
resolvedAsset.asset,
|
||||
resolvedAsset.timelineService,
|
||||
resolvedAsset.initialIndex,
|
||||
viewIntentFilePath: resolvedAsset.viewIntentFilePath,
|
||||
);
|
||||
}
|
||||
|
||||
void _openAssetViewer(
|
||||
BaseAsset asset,
|
||||
TimelineService timelineService,
|
||||
int initialIndex, {
|
||||
String? viewIntentFilePath,
|
||||
}) {
|
||||
_ref.read(assetViewerProvider.notifier).reset();
|
||||
_ref.read(assetViewerProvider.notifier).setAsset(asset);
|
||||
if (viewIntentFilePath != null) {
|
||||
_ref.read(viewIntentFilePathProvider.notifier).setPath(viewIntentFilePath);
|
||||
unawaited(_viewIntentService.setManagedTempFilePath(viewIntentFilePath));
|
||||
} else {
|
||||
_ref.read(viewIntentFilePathProvider.notifier).clear();
|
||||
unawaited(_viewIntentService.cleanupManagedTempFile());
|
||||
}
|
||||
|
||||
if (asset.isVideo) {
|
||||
_ref.read(assetViewerProvider.notifier).setControls(true);
|
||||
}
|
||||
|
||||
_router.push(AssetViewerRoute(initialIndex: initialIndex, timelineService: timelineService));
|
||||
}
|
||||
Future<void> handle(ViewIntentPayload attachment);
|
||||
}
|
||||
|
||||
final viewIntentHandlerProvider = Provider<ViewIntentHandler>((ref) {
|
||||
if (Platform.isAndroid) {
|
||||
return AndroidViewIntentHandler(ref);
|
||||
}
|
||||
|
||||
return const StubViewIntentHandler();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'dart:async';
|
||||
|
||||
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/platform/view_intent_api.g.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/view_intent/view_intent_file_path.provider.dart';
|
||||
import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart';
|
||||
import 'package:immich_mobile/providers/view_intent/view_intent_pending.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/view_intent.service.dart';
|
||||
import 'package:immich_mobile/services/view_intent_asset_resolver.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class AndroidViewIntentHandler implements ViewIntentHandler {
|
||||
final Ref _ref;
|
||||
final ViewIntentService _viewIntentService;
|
||||
final ViewIntentAssetResolver _viewIntentAssetResolver;
|
||||
final AppRouter _router;
|
||||
static final Logger _logger = Logger('ViewIntentHandler');
|
||||
|
||||
AndroidViewIntentHandler(Ref ref)
|
||||
: _ref = ref,
|
||||
_viewIntentService = ref.read(viewIntentServiceProvider),
|
||||
_viewIntentAssetResolver = ref.read(viewIntentAssetResolverProvider),
|
||||
_router = ref.watch(appRouterProvider);
|
||||
|
||||
@override
|
||||
void init() {
|
||||
// Covers cold start from a view intent before the first lifecycle "resumed".
|
||||
unawaited(onAppResumed());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onAppResumed() => _checkForViewIntent();
|
||||
|
||||
@override
|
||||
Future<void> onUserAuthenticated() => _flushPending();
|
||||
|
||||
Future<void> _checkForViewIntent() async {
|
||||
final attachment = await _viewIntentService.consumeViewIntent();
|
||||
if (attachment != null) {
|
||||
await handle(attachment);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_ref.read(viewIntentPendingProvider) == null) {
|
||||
await _viewIntentService.cleanupStaleTempFiles();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _flushPending() async {
|
||||
final pendingAttachment = _ref.read(viewIntentPendingProvider.notifier).takeIfFresh();
|
||||
_logger.info('flushPending, pendingAttachment:$pendingAttachment}');
|
||||
if (pendingAttachment != null) {
|
||||
await handle(pendingAttachment);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> handle(ViewIntentPayload attachment) async {
|
||||
_logger.info(
|
||||
'handle attachment, mimeType:${attachment.mimeType}, localAssetId=${attachment.localAssetId}, path=${attachment.path}, isAuthenticated:${_ref.read(authProvider).isAuthenticated}',
|
||||
);
|
||||
|
||||
if (!_ref.read(authProvider).isAuthenticated) {
|
||||
_ref.read(viewIntentPendingProvider.notifier).defer(attachment);
|
||||
return;
|
||||
}
|
||||
|
||||
final resolvedAsset = await _viewIntentAssetResolver.resolve(attachment);
|
||||
_logger.fine('resolved view intent asset: ${resolvedAsset.asset}');
|
||||
_openAssetViewer(
|
||||
resolvedAsset.asset,
|
||||
resolvedAsset.timelineService,
|
||||
resolvedAsset.initialIndex,
|
||||
viewIntentFilePath: resolvedAsset.viewIntentFilePath,
|
||||
);
|
||||
}
|
||||
|
||||
void _openAssetViewer(
|
||||
BaseAsset asset,
|
||||
TimelineService timelineService,
|
||||
int initialIndex, {
|
||||
String? viewIntentFilePath,
|
||||
}) {
|
||||
_ref.read(assetViewerProvider.notifier).reset();
|
||||
_ref.read(assetViewerProvider.notifier).setAsset(asset);
|
||||
if (viewIntentFilePath != null) {
|
||||
_ref.read(viewIntentFilePathProvider.notifier).setPath(viewIntentFilePath);
|
||||
unawaited(_viewIntentService.setManagedTempFilePath(viewIntentFilePath));
|
||||
} else {
|
||||
_ref.read(viewIntentFilePathProvider.notifier).clear();
|
||||
unawaited(_viewIntentService.cleanupManagedTempFile());
|
||||
}
|
||||
|
||||
if (asset.isVideo) {
|
||||
_ref.read(assetViewerProvider.notifier).setControls(true);
|
||||
}
|
||||
|
||||
_router.push(AssetViewerRoute(initialIndex: initialIndex, timelineService: timelineService));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:immich_mobile/platform/view_intent_api.g.dart';
|
||||
import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart';
|
||||
|
||||
class StubViewIntentHandler implements ViewIntentHandler {
|
||||
const StubViewIntentHandler();
|
||||
|
||||
@override
|
||||
void init() {}
|
||||
|
||||
@override
|
||||
Future<void> onAppResumed() async {}
|
||||
|
||||
@override
|
||||
Future<void> onUserAuthenticated() async {}
|
||||
|
||||
@override
|
||||
Future<void> handle(ViewIntentPayload attachment) async {}
|
||||
}
|
||||
@@ -253,7 +253,7 @@ class LoginForm extends HookConsumerWidget {
|
||||
ref.read(websocketProvider.notifier).connect();
|
||||
unawaited(context.replaceRoute(const TabShellRoute()));
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
unawaited(ref.read(viewIntentHandlerProvider).flushPending());
|
||||
unawaited(ref.read(viewIntentHandlerProvider).onUserAuthenticated());
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -343,7 +343,7 @@ class LoginForm extends HookConsumerWidget {
|
||||
unawaited(handleSyncFlow());
|
||||
unawaited(context.replaceRoute(const TabShellRoute()));
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
unawaited(ref.read(viewIntentHandlerProvider).flushPending());
|
||||
unawaited(ref.read(viewIntentHandlerProvider).onUserAuthenticated());
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user