From 2129f889f59833db239ffc9edf3c200e79ee1698 Mon Sep 17 00:00:00 2001 From: idubnori Date: Fri, 24 Oct 2025 23:02:56 +0900 Subject: [PATCH] feat: (mobile) open asset viewer from album activity page (#23182) * feat(mobile): open assetviewer via album activities page * adjust ui behavior: keep current asset & disable initial forcus * fix: Run 'make build' and 'make pigeon' --- .../lib/domain/services/timeline.service.dart | 1 + .../pages/drift_activities.page.dart | 2 +- .../album/drift_activity_text_field.dart | 4 --- .../providers/activity_service.provider.dart | 8 ++++- .../activity_service.provider.g.dart | 2 +- mobile/lib/services/activity.service.dart | 27 +++++++++++++- .../lib/widgets/activities/activity_tile.dart | 36 +++++++++++++------ 7 files changed, 61 insertions(+), 19 deletions(-) diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index 85fc5fc55d..9537fe667a 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -33,6 +33,7 @@ enum TimelineOrigin { map, search, deepLink, + albumActivities, } class TimelineFactory { diff --git a/mobile/lib/presentation/pages/drift_activities.page.dart b/mobile/lib/presentation/pages/drift_activities.page.dart index 8e67d85884..d8f8799f7d 100644 --- a/mobile/lib/presentation/pages/drift_activities.page.dart +++ b/mobile/lib/presentation/pages/drift_activities.page.dart @@ -23,7 +23,7 @@ class DriftActivitiesPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final album = ref.watch(currentRemoteAlbumProvider)!; - final asset = ref.watch(currentAssetNotifier) as RemoteAsset?; + final asset = ref.read(currentAssetNotifier) as RemoteAsset?; final user = ref.watch(currentUserProvider); final activityNotifier = ref.read(albumActivityProvider(album.id, asset?.id).notifier); diff --git a/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart b/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart index 86a0c80345..fe5c763ec5 100644 --- a/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart +++ b/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart @@ -36,10 +36,6 @@ class _DriftActivityTextFieldState extends ConsumerState inputController = TextEditingController(); inputFocusNode = FocusNode(); - if (!widget.isBottomSheet) { - inputFocusNode.requestFocus(); - } - inputFocusNode.addListener(() { if (inputFocusNode.hasFocus) { widget.onKeyboardFocus?.call(); diff --git a/mobile/lib/providers/activity_service.provider.dart b/mobile/lib/providers/activity_service.provider.dart index a7fd0715f8..f17617bced 100644 --- a/mobile/lib/providers/activity_service.provider.dart +++ b/mobile/lib/providers/activity_service.provider.dart @@ -1,4 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/repositories/activity_api.repository.dart'; import 'package:immich_mobile/services/activity.service.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -6,4 +8,8 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'activity_service.provider.g.dart'; @riverpod -ActivityService activityService(Ref ref) => ActivityService(ref.watch(activityApiRepositoryProvider)); +ActivityService activityService(Ref ref) => ActivityService( + ref.watch(activityApiRepositoryProvider), + ref.watch(timelineFactoryProvider), + ref.watch(assetServiceProvider), +); diff --git a/mobile/lib/providers/activity_service.provider.g.dart b/mobile/lib/providers/activity_service.provider.g.dart index 2bf160c487..4641738fc4 100644 --- a/mobile/lib/providers/activity_service.provider.g.dart +++ b/mobile/lib/providers/activity_service.provider.g.dart @@ -6,7 +6,7 @@ part of 'activity_service.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$activityServiceHash() => r'ce775779787588defe1e76406e09a9c109470310'; +String _$activityServiceHash() => r'3ce0eb33948138057cc63f07a7598047b99e7599'; /// See also [activityService]. @ProviderFor(activityService) diff --git a/mobile/lib/services/activity.service.dart b/mobile/lib/services/activity.service.dart index 1f09309947..09abde20e0 100644 --- a/mobile/lib/services/activity.service.dart +++ b/mobile/lib/services/activity.service.dart @@ -1,16 +1,24 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/errors.dart'; +import 'package:immich_mobile/domain/services/asset.service.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/mixins/error_logger.mixin.dart'; import 'package:immich_mobile/models/activities/activity.model.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; import 'package:immich_mobile/repositories/activity_api.repository.dart'; +import 'package:immich_mobile/routing/router.dart'; import 'package:logging/logging.dart'; +import 'package:immich_mobile/entities/store.entity.dart' as immich_store; class ActivityService with ErrorLoggerMixin { final ActivityApiRepository _activityApiRepository; + final TimelineFactory _timelineFactory; + final AssetService _assetService; @override final Logger logger = Logger("ActivityService"); - ActivityService(this._activityApiRepository); + ActivityService(this._activityApiRepository, this._timelineFactory, this._assetService); Future> getAllActivities(String albumId, {String? assetId}) async { return logError( @@ -49,4 +57,21 @@ class ActivityService with ErrorLoggerMixin { errorMessage: "Failed to create $type for album $albumId", ); } + + Future buildAssetViewerRoute(String assetId, WidgetRef ref) async { + if (immich_store.Store.isBetaTimelineEnabled) { + final asset = await _assetService.getRemoteAsset(assetId); + if (asset == null) { + return null; + } + + AssetViewer.setAsset(ref, asset); + return AssetViewerRoute( + initialIndex: 0, + timelineService: _timelineFactory.fromAssets([asset], TimelineOrigin.albumActivities), + ); + } + + return null; + } } diff --git a/mobile/lib/widgets/activities/activity_tile.dart b/mobile/lib/widgets/activities/activity_tile.dart index e0cced0d22..6812d1b90c 100644 --- a/mobile/lib/widgets/activities/activity_tile.dart +++ b/mobile/lib/widgets/activities/activity_tile.dart @@ -1,8 +1,10 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/datetime_extensions.dart'; import 'package:immich_mobile/models/activities/activity.model.dart'; +import 'package:immich_mobile/providers/activity_service.provider.dart'; import 'package:immich_mobile/providers/image/immich_remote_thumbnail_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; @@ -21,6 +23,14 @@ class ActivityTile extends HookConsumerWidget { // currentAssetProvider will not be set until we open the gallery viewer final showAssetThumbnail = asset == null && activity.assetId != null && !isBottomSheet; + onTap() async { + final activityService = ref.read(activityServiceProvider); + final route = await activityService.buildAssetViewerRoute(activity.assetId!, ref); + if (route != null) { + await context.pushRoute(route); + } + } + return ListTile( minVerticalPadding: 15, leading: isLike @@ -39,7 +49,7 @@ class ActivityTile extends HookConsumerWidget { ), // No subtitle for like, so center title titleAlignment: !isLike ? ListTileTitleAlignment.top : ListTileTitleAlignment.center, - trailing: showAssetThumbnail ? _ActivityAssetThumbnail(activity.assetId!) : null, + trailing: showAssetThumbnail ? _ActivityAssetThumbnail(activity.assetId!, onTap) : null, subtitle: !isLike ? Text(activity.comment!) : null, ); } @@ -78,22 +88,26 @@ class _ActivityTitle extends StatelessWidget { class _ActivityAssetThumbnail extends StatelessWidget { final String assetId; + final GestureTapCallback? onTap; - const _ActivityAssetThumbnail(this.assetId); + const _ActivityAssetThumbnail(this.assetId, this.onTap); @override Widget build(BuildContext context) { - return Container( - width: 40, - height: 30, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(4)), - image: DecorationImage( - image: ImmichRemoteThumbnailProvider(assetId: assetId), - fit: BoxFit.cover, + return GestureDetector( + onTap: onTap, + child: Container( + width: 40, + height: 30, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(4)), + image: DecorationImage( + image: ImmichRemoteThumbnailProvider(assetId: assetId), + fit: BoxFit.cover, + ), ), + child: const SizedBox.shrink(), ), - child: const SizedBox.shrink(), ); } }