chore: refactor show view in timeline button

This refactor includes changes to notify asset viewer about where an asset was shown from.
This commit is contained in:
bwees 2025-10-12 23:54:44 -05:00
parent f2b553182a
commit 268867353b
No known key found for this signature in database
8 changed files with 77 additions and 22 deletions

View file

@ -16,7 +16,24 @@ typedef TimelineAssetSource = Future<List<BaseAsset>> Function(int index, int co
typedef TimelineBucketSource = Stream<List<Bucket>> Function();
typedef TimelineQuery = ({TimelineAssetSource assetSource, TimelineBucketSource bucketSource});
typedef TimelineQuery = ({TimelineAssetSource assetSource, TimelineBucketSource bucketSource, TimelineType type});
enum TimelineType {
main,
localAlbum,
remoteAlbum,
remoteAssets,
favorite,
trash,
archive,
lockedFolder,
video,
place,
person,
map,
search,
deepLink,
}
class TimelineFactory {
final DriftTimelineRepository _timelineRepository;
@ -57,7 +74,8 @@ class TimelineFactory {
TimelineService person(String userId, String personId) =>
TimelineService(_timelineRepository.person(userId, personId, groupBy));
TimelineService fromAssets(List<BaseAsset> assets) => TimelineService(_timelineRepository.fromAssets(assets));
TimelineService fromAssets(List<BaseAsset> assets, TimelineType type) =>
TimelineService(_timelineRepository.fromAssets(assets, type));
TimelineService map(String userId, LatLngBounds bounds) =>
TimelineService(_timelineRepository.map(userId, bounds, groupBy));
@ -66,6 +84,7 @@ class TimelineFactory {
class TimelineService {
final TimelineAssetSource _assetSource;
final TimelineBucketSource _bucketSource;
final TimelineType type;
final AsyncMutex _mutex = AsyncMutex();
int _bufferOffset = 0;
List<BaseAsset> _buffer = [];
@ -74,10 +93,14 @@ class TimelineService {
int _totalAssets = 0;
int get totalAssets => _totalAssets;
TimelineService(TimelineQuery query) : this._(assetSource: query.assetSource, bucketSource: query.bucketSource);
TimelineService(TimelineQuery query)
: this._(assetSource: query.assetSource, bucketSource: query.bucketSource, type: query.type);
TimelineService._({required TimelineAssetSource assetSource, required TimelineBucketSource bucketSource})
: _assetSource = assetSource,
TimelineService._({
required TimelineAssetSource assetSource,
required TimelineBucketSource bucketSource,
required this.type,
}) : _assetSource = assetSource,
_bucketSource = bucketSource {
_bucketSubscription = _bucketSource().listen((buckets) {
_mutex.run(() async {

View file

@ -35,6 +35,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
TimelineQuery main(List<String> userIds, GroupAssetsBy groupBy) => (
bucketSource: () => _watchMainBucket(userIds, groupBy: groupBy),
assetSource: (offset, count) => _getMainBucketAssets(userIds, offset: offset, count: count),
type: TimelineType.main,
);
Stream<List<Bucket>> _watchMainBucket(List<String> userIds, {GroupAssetsBy groupBy = GroupAssetsBy.day}) {
@ -91,6 +92,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
TimelineQuery localAlbum(String albumId, GroupAssetsBy groupBy) => (
bucketSource: () => _watchLocalAlbumBucket(albumId, groupBy: groupBy),
assetSource: (offset, count) => _getLocalAlbumBucketAssets(albumId, offset: offset, count: count),
type: TimelineType.localAlbum,
);
Stream<List<Bucket>> _watchLocalAlbumBucket(String albumId, {GroupAssetsBy groupBy = GroupAssetsBy.day}) {
@ -156,6 +158,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
TimelineQuery remoteAlbum(String albumId, GroupAssetsBy groupBy) => (
bucketSource: () => _watchRemoteAlbumBucket(albumId, groupBy: groupBy),
assetSource: (offset, count) => _getRemoteAlbumBucketAssets(albumId, offset: offset, count: count),
type: TimelineType.remoteAlbum,
);
Stream<List<Bucket>> _watchRemoteAlbumBucket(String albumId, {GroupAssetsBy groupBy = GroupAssetsBy.day}) {
@ -244,15 +247,17 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
.get();
}
TimelineQuery fromAssets(List<BaseAsset> assets) => (
TimelineQuery fromAssets(List<BaseAsset> assets, TimelineType type) => (
bucketSource: () => Stream.value(_generateBuckets(assets.length)),
assetSource: (offset, count) => Future.value(assets.skip(offset).take(count).toList(growable: false)),
type: type,
);
TimelineQuery remote(String ownerId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
filter: (row) =>
row.deletedAt.isNull() & row.visibility.equalsValue(AssetVisibility.timeline) & row.ownerId.equals(ownerId),
groupBy: groupBy,
type: TimelineType.remoteAssets,
);
TimelineQuery favorite(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
@ -262,11 +267,13 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
row.ownerId.equals(userId) &
row.visibility.equalsValue(AssetVisibility.timeline),
groupBy: groupBy,
type: TimelineType.favorite,
);
TimelineQuery trash(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
filter: (row) => row.deletedAt.isNotNull() & row.ownerId.equals(userId),
groupBy: groupBy,
type: TimelineType.trash,
joinLocal: true,
);
@ -274,11 +281,13 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
filter: (row) =>
row.deletedAt.isNull() & row.ownerId.equals(userId) & row.visibility.equalsValue(AssetVisibility.archive),
groupBy: groupBy,
type: TimelineType.archive,
);
TimelineQuery locked(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
filter: (row) =>
row.deletedAt.isNull() & row.visibility.equalsValue(AssetVisibility.locked) & row.ownerId.equals(userId),
type: TimelineType.lockedFolder,
groupBy: groupBy,
);
@ -288,17 +297,20 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
row.type.equalsValue(AssetType.video) &
row.visibility.equalsValue(AssetVisibility.timeline) &
row.ownerId.equals(userId),
type: TimelineType.video,
groupBy: groupBy,
);
TimelineQuery place(String place, GroupAssetsBy groupBy) => (
bucketSource: () => _watchPlaceBucket(place, groupBy: groupBy),
assetSource: (offset, count) => _getPlaceBucketAssets(place, offset: offset, count: count),
type: TimelineType.place,
);
TimelineQuery person(String userId, String personId, GroupAssetsBy groupBy) => (
bucketSource: () => _watchPersonBucket(userId, personId, groupBy: groupBy),
assetSource: (offset, count) => _getPersonBucketAssets(userId, personId, offset: offset, count: count),
type: TimelineType.person,
);
Stream<List<Bucket>> _watchPlaceBucket(String place, {GroupAssetsBy groupBy = GroupAssetsBy.day}) {
@ -434,6 +446,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
TimelineQuery map(String userId, LatLngBounds bounds, GroupAssetsBy groupBy) => (
bucketSource: () => _watchMapBucket(userId, bounds, groupBy: groupBy),
assetSource: (offset, count) => _getMapBucketAssets(userId, bounds, offset: offset, count: count),
type: TimelineType.map,
);
Stream<List<Bucket>> _watchMapBucket(
@ -502,6 +515,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
@pragma('vm:prefer-inline')
TimelineQuery _remoteQueryBuilder({
required Expression<bool> Function($RemoteAssetEntityTable row) filter,
required TimelineType type,
GroupAssetsBy groupBy = GroupAssetsBy.day,
bool joinLocal = false,
}) {
@ -509,6 +523,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
bucketSource: () => _watchRemoteBucket(filter: filter, groupBy: groupBy),
assetSource: (offset, count) =>
_getRemoteAssets(filter: filter, offset: offset, count: count, joinLocal: joinLocal),
type: type,
);
}

View file

@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/person.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
@ -624,7 +625,7 @@ class _SearchResultGrid extends ConsumerWidget {
child: ProviderScope(
overrides: [
timelineServiceProvider.overrideWith((ref) {
final timelineService = ref.watch(timelineFactoryProvider).fromAssets(assets);
final timelineService = ref.watch(timelineFactoryProvider).fromAssets(assets, TimelineType.search);
ref.onDispose(timelineService.dispose);
return timelineService;
}),

View file

@ -61,8 +61,9 @@ class AssetViewer extends ConsumerStatefulWidget {
@override
ConsumerState createState() => _AssetViewerState();
static void setAsset(WidgetRef ref, BaseAsset asset) {
static void setAsset(WidgetRef ref, BaseAsset asset, TimelineType timelineType) {
ref.read(assetViewerProvider.notifier).reset();
ref.read(assetViewerProvider.notifier).setTimelineType(timelineType);
_setAsset(ref, asset);
}

View file

@ -1,4 +1,5 @@
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -17,6 +18,7 @@ class AssetViewerState {
final bool showingControls;
final BaseAsset? currentAsset;
final int stackIndex;
final TimelineType timelineType;
const AssetViewerState({
this.backgroundOpacity = 255,
@ -24,6 +26,7 @@ class AssetViewerState {
this.showingControls = true,
this.currentAsset,
this.stackIndex = 0,
this.timelineType = TimelineType.main,
});
AssetViewerState copyWith({
@ -32,6 +35,7 @@ class AssetViewerState {
bool? showingControls,
BaseAsset? currentAsset,
int? stackIndex,
TimelineType? timelineType,
}) {
return AssetViewerState(
backgroundOpacity: backgroundOpacity ?? this.backgroundOpacity,
@ -39,6 +43,7 @@ class AssetViewerState {
showingControls: showingControls ?? this.showingControls,
currentAsset: currentAsset ?? this.currentAsset,
stackIndex: stackIndex ?? this.stackIndex,
timelineType: timelineType ?? this.timelineType,
);
}
@ -119,6 +124,13 @@ class AssetViewerStateNotifier extends Notifier<AssetViewerState> {
}
state = state.copyWith(stackIndex: index);
}
void setTimelineType(TimelineType type) {
if (type == state.timelineType) {
return;
}
state = state.copyWith(timelineType: type);
}
}
final assetViewerProvider = NotifierProvider<AssetViewerStateNotifier, AssetViewerState>(AssetViewerStateNotifier.new);

View file

@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
@ -18,7 +19,6 @@ import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asse
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/tab.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
@ -39,13 +39,11 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
final isInLockedView = ref.watch(inLockedViewProvider);
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
final previousRouteName = ref.watch(previousRouteNameProvider);
final tabRoute = ref.watch(tabProvider);
final timelineType = ref.read(assetViewerProvider).timelineType;
final showViewInTimelineButton =
(previousRouteName != TabShellRoute.name || tabRoute == TabEnum.search) &&
previousRouteName != AssetViewerRoute.name &&
previousRouteName != null &&
previousRouteName != LocalTimelineRoute.name;
timelineType != TimelineType.main &&
timelineType != TimelineType.deepLink &&
timelineType != TimelineType.localAlbum;
final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));

View file

@ -15,8 +15,8 @@ import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart'
import 'package:immich_mobile/presentation/widgets/timeline/timeline_drag_region.dart';
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/routing/router.dart';
@ -134,6 +134,7 @@ class _FixedSegmentRow extends ConsumerWidget {
key: ValueKey(Object.hash(assets[i].heroTag, assetIndex + i, timelineService.hashCode)),
asset: assets[i],
assetIndex: assetIndex + i,
timelineType: timelineService.type,
),
),
],
@ -144,8 +145,9 @@ class _FixedSegmentRow extends ConsumerWidget {
class _AssetTileWidget extends ConsumerWidget {
final BaseAsset asset;
final int assetIndex;
final TimelineType timelineType;
const _AssetTileWidget({super.key, required this.asset, required this.assetIndex});
const _AssetTileWidget({super.key, required this.asset, required this.assetIndex, required this.timelineType});
Future _handleOnTap(BuildContext ctx, WidgetRef ref, int assetIndex, BaseAsset asset, int? heroOffset) async {
final multiSelectState = ref.read(multiSelectProvider);
@ -155,7 +157,7 @@ class _AssetTileWidget extends ConsumerWidget {
} else {
await ref.read(timelineServiceProvider).loadAssets(assetIndex, 1);
ref.read(isPlayingMotionVideoProvider.notifier).playing = false;
AssetViewer.setAsset(ref, asset);
AssetViewer.setAsset(ref, asset, timelineType);
ctx.pushRoute(
AssetViewerRoute(
initialIndex: assetIndex,

View file

@ -8,8 +8,8 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart' as beta_asset_provider;
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart' as beta_asset_provider;
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
@ -149,8 +149,11 @@ class DeepLinkService {
return null;
}
AssetViewer.setAsset(ref, asset);
return AssetViewerRoute(initialIndex: 0, timelineService: _betaTimelineFactory.fromAssets([asset]));
AssetViewer.setAsset(ref, asset, TimelineType.deepLink);
return AssetViewerRoute(
initialIndex: 0,
timelineService: _betaTimelineFactory.fromAssets([asset], TimelineType.deepLink),
);
} else {
// TODO: Remove this when beta is default
final asset = await _assetService.getAssetByRemoteId(assetId);