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'
This commit is contained in:
idubnori 2025-10-24 23:02:56 +09:00 committed by GitHub
parent 221e0ef02f
commit 2129f889f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 61 additions and 19 deletions

View file

@ -33,6 +33,7 @@ enum TimelineOrigin {
map,
search,
deepLink,
albumActivities,
}
class TimelineFactory {

View file

@ -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);

View file

@ -36,10 +36,6 @@ class _DriftActivityTextFieldState extends ConsumerState<DriftActivityTextField>
inputController = TextEditingController();
inputFocusNode = FocusNode();
if (!widget.isBottomSheet) {
inputFocusNode.requestFocus();
}
inputFocusNode.addListener(() {
if (inputFocusNode.hasFocus) {
widget.onKeyboardFocus?.call();

View file

@ -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),
);

View file

@ -6,7 +6,7 @@ part of 'activity_service.provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$activityServiceHash() => r'ce775779787588defe1e76406e09a9c109470310';
String _$activityServiceHash() => r'3ce0eb33948138057cc63f07a7598047b99e7599';
/// See also [activityService].
@ProviderFor(activityService)

View file

@ -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<List<Activity>> 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<AssetViewerRoute?> 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;
}
}

View file

@ -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(),
);
}
}