mirror of
https://github.com/samsonjs/immich.git
synced 2026-03-25 09:15:56 +00:00
feat: handle live photos on new asset viewer (#19926)
sync and handle livePhotoVideoId in asset viewer Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
parent
805ec3e351
commit
9abb95d34a
14 changed files with 53 additions and 8 deletions
BIN
mobile/drift_schemas/main/drift_schema_v1.json
generated
BIN
mobile/drift_schemas/main/drift_schema_v1.json
generated
Binary file not shown.
|
|
@ -43,6 +43,8 @@ sealed class BaseAsset {
|
|||
bool get isImage => type == AssetType.image;
|
||||
bool get isVideo => type == AssetType.video;
|
||||
|
||||
bool get isMotionPhoto => livePhotoVideoId != null;
|
||||
|
||||
Duration get duration {
|
||||
final durationInSeconds = this.durationInSeconds;
|
||||
if (durationInSeconds != null) {
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ class RemoteAsset extends BaseAsset {
|
|||
bool? isFavorite,
|
||||
String? thumbHash,
|
||||
AssetVisibility? visibility,
|
||||
String? livePhotoVideoId,
|
||||
}) {
|
||||
return RemoteAsset(
|
||||
id: id ?? this.id,
|
||||
|
|
@ -110,6 +111,7 @@ class RemoteAsset extends BaseAsset {
|
|||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
thumbHash: thumbHash ?? this.thumbHash,
|
||||
visibility: visibility ?? this.visibility,
|
||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ mergedAsset: SELECT * FROM
|
|||
rae.thumb_hash,
|
||||
rae.checksum,
|
||||
rae.owner_id,
|
||||
rae.live_photo_video_id,
|
||||
0 as orientation
|
||||
FROM
|
||||
remote_asset_entity rae
|
||||
|
|
@ -39,6 +40,7 @@ mergedAsset: SELECT * FROM
|
|||
NULL as thumb_hash,
|
||||
lae.checksum,
|
||||
NULL as owner_id,
|
||||
NULL as live_photo_video_id,
|
||||
lae.orientation
|
||||
FROM
|
||||
local_asset_entity lae
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -30,6 +30,8 @@ class RemoteAssetEntity extends Table
|
|||
|
||||
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||
|
||||
TextColumn get livePhotoVideoId => text().nullable()();
|
||||
|
||||
IntColumn get visibility => intEnum<AssetVisibility>()();
|
||||
|
||||
@override
|
||||
|
|
@ -51,6 +53,7 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
|
|||
width: width,
|
||||
thumbHash: thumbHash,
|
||||
visibility: visibility,
|
||||
livePhotoVideoId: livePhotoVideoId,
|
||||
localId: null,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -5,13 +5,13 @@ import 'package:immich_mobile/domain/models/album/album.model.dart';
|
|||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
|
|
@ -134,6 +134,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||
thumbHash: Value(asset.thumbhash),
|
||||
deletedAt: Value(asset.deletedAt),
|
||||
visibility: Value(asset.visibility.toAssetVisibility()),
|
||||
livePhotoVideoId: Value(asset.livePhotoVideoId),
|
||||
);
|
||||
|
||||
batch.insert(
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||
height: row.height,
|
||||
isFavorite: row.isFavorite,
|
||||
durationInSeconds: row.durationInSeconds,
|
||||
livePhotoVideoId: row.livePhotoVideoId,
|
||||
)
|
||||
: LocalAsset(
|
||||
id: row.localId!,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
||||
|
||||
class MotionPhotoActionButton extends ConsumerWidget {
|
||||
const MotionPhotoActionButton({super.key, this.menuItem = true});
|
||||
|
||||
final bool menuItem;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isPlaying = ref.watch(isPlayingMotionVideoProvider);
|
||||
|
||||
return BaseActionButton(
|
||||
iconData: isPlaying
|
||||
? Icons.motion_photos_pause_outlined
|
||||
: Icons.play_circle_outline_rounded,
|
||||
label: "play_motion_photo".t(context: context),
|
||||
onPressed: ref.read(isPlayingMotionVideoProvider.notifier).toggle,
|
||||
menuItem: menuItem,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/top_app_bar.widg
|
|||
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
|
|
@ -165,7 +166,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
void _onAssetChanged(int index) {
|
||||
final asset = ref.read(timelineServiceProvider).getAsset(index);
|
||||
ref.read(currentAssetNotifier.notifier).setAsset(asset);
|
||||
if (asset.isVideo) {
|
||||
if (asset.isVideo || asset.isMotionPhoto) {
|
||||
ref.read(videoPlaybackValueProvider.notifier).reset();
|
||||
ref.read(videoPlayerControlsProvider.notifier).pause();
|
||||
}
|
||||
|
|
@ -473,11 +474,16 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
}
|
||||
}
|
||||
|
||||
void _onLongPress(_, __, ___) {
|
||||
ref.read(isPlayingMotionVideoProvider.notifier).playing = true;
|
||||
}
|
||||
|
||||
PhotoViewGalleryPageOptions _assetBuilder(BuildContext ctx, int index) {
|
||||
scaffoldContext ??= ctx;
|
||||
final asset = ref.read(timelineServiceProvider).getAsset(index);
|
||||
final isPlayingMotionVideo = ref.read(isPlayingMotionVideoProvider);
|
||||
|
||||
if (asset.isImage) {
|
||||
if (asset.isImage && !isPlayingMotionVideo) {
|
||||
return _imageBuilder(ctx, asset);
|
||||
}
|
||||
|
||||
|
|
@ -500,6 +506,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
onDragUpdate: _onDragUpdate,
|
||||
onDragEnd: _onDragEnd,
|
||||
onTapDown: _onTapDown,
|
||||
onLongPressStart: asset.isMotionPhoto ? _onLongPress : null,
|
||||
errorBuilder: (_, __, ___) => Container(
|
||||
width: ctx.width,
|
||||
height: ctx.height,
|
||||
|
|
@ -561,6 +568,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
// Using multiple selectors to avoid unnecessary rebuilds for other state changes
|
||||
ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet));
|
||||
ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity));
|
||||
ref.watch(isPlayingMotionVideoProvider);
|
||||
|
||||
// Currently it is not possible to scroll the asset when the bottom sheet is open all the way.
|
||||
// Issue: https://github.com/flutter/flutter/issues/109037
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
|
|
@ -44,6 +45,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||
source: ActionSource.viewer,
|
||||
menuItem: true,
|
||||
),
|
||||
if (asset.isMotionPhoto) const MotionPhotoActionButton(menuItem: true),
|
||||
const _KebabMenu(),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -270,10 +270,7 @@ class NativeVideoViewer extends HookConsumerWidget {
|
|||
return;
|
||||
}
|
||||
|
||||
if (videoController.playbackInfo?.status == PlaybackStatus.stopped &&
|
||||
!ref
|
||||
.read(appSettingsServiceProvider)
|
||||
.getSetting<bool>(AppSettingsEnum.loopVideo)) {
|
||||
if (videoController.playbackInfo?.status == PlaybackStatus.stopped) {
|
||||
ref.read(isPlayingMotionVideoProvider.notifier).playing = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -310,7 +307,7 @@ class NativeVideoViewer extends HookConsumerWidget {
|
|||
final loopVideo = ref
|
||||
.read(appSettingsServiceProvider)
|
||||
.getSetting<bool>(AppSettingsEnum.loopVideo);
|
||||
nc.setLoop(loopVideo);
|
||||
nc.setLoop(!asset.isMotionPhoto && loopVideo);
|
||||
|
||||
controller.value = nc;
|
||||
Timer(const Duration(milliseconds: 200), checkIfBuffering);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:immich_mobile/presentation/widgets/timeline/header.widget.dart';
|
|||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/segment_builder.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.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/timeline/multiselect.provider.dart';
|
||||
|
|
@ -171,6 +172,7 @@ class _AssetTileWidget extends ConsumerWidget {
|
|||
ref.read(multiSelectProvider.notifier).toggleAssetSelection(asset);
|
||||
} else {
|
||||
await ref.read(timelineServiceProvider).loadAssets(assetIndex, 1);
|
||||
ref.read(isPlayingMotionVideoProvider.notifier).playing = false;
|
||||
ctx.pushRoute(
|
||||
AssetViewerRoute(
|
||||
initialIndex: assetIndex,
|
||||
|
|
|
|||
Loading…
Reference in a new issue