mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +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 isImage => type == AssetType.image;
|
||||||
bool get isVideo => type == AssetType.video;
|
bool get isVideo => type == AssetType.video;
|
||||||
|
|
||||||
|
bool get isMotionPhoto => livePhotoVideoId != null;
|
||||||
|
|
||||||
Duration get duration {
|
Duration get duration {
|
||||||
final durationInSeconds = this.durationInSeconds;
|
final durationInSeconds = this.durationInSeconds;
|
||||||
if (durationInSeconds != null) {
|
if (durationInSeconds != null) {
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ class RemoteAsset extends BaseAsset {
|
||||||
bool? isFavorite,
|
bool? isFavorite,
|
||||||
String? thumbHash,
|
String? thumbHash,
|
||||||
AssetVisibility? visibility,
|
AssetVisibility? visibility,
|
||||||
|
String? livePhotoVideoId,
|
||||||
}) {
|
}) {
|
||||||
return RemoteAsset(
|
return RemoteAsset(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
|
|
@ -110,6 +111,7 @@ class RemoteAsset extends BaseAsset {
|
||||||
isFavorite: isFavorite ?? this.isFavorite,
|
isFavorite: isFavorite ?? this.isFavorite,
|
||||||
thumbHash: thumbHash ?? this.thumbHash,
|
thumbHash: thumbHash ?? this.thumbHash,
|
||||||
visibility: visibility ?? this.visibility,
|
visibility: visibility ?? this.visibility,
|
||||||
|
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ mergedAsset: SELECT * FROM
|
||||||
rae.thumb_hash,
|
rae.thumb_hash,
|
||||||
rae.checksum,
|
rae.checksum,
|
||||||
rae.owner_id,
|
rae.owner_id,
|
||||||
|
rae.live_photo_video_id,
|
||||||
0 as orientation
|
0 as orientation
|
||||||
FROM
|
FROM
|
||||||
remote_asset_entity rae
|
remote_asset_entity rae
|
||||||
|
|
@ -39,6 +40,7 @@ mergedAsset: SELECT * FROM
|
||||||
NULL as thumb_hash,
|
NULL as thumb_hash,
|
||||||
lae.checksum,
|
lae.checksum,
|
||||||
NULL as owner_id,
|
NULL as owner_id,
|
||||||
|
NULL as live_photo_video_id,
|
||||||
lae.orientation
|
lae.orientation
|
||||||
FROM
|
FROM
|
||||||
local_asset_entity lae
|
local_asset_entity lae
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -30,6 +30,8 @@ class RemoteAssetEntity extends Table
|
||||||
|
|
||||||
DateTimeColumn get deletedAt => dateTime().nullable()();
|
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||||
|
|
||||||
|
TextColumn get livePhotoVideoId => text().nullable()();
|
||||||
|
|
||||||
IntColumn get visibility => intEnum<AssetVisibility>()();
|
IntColumn get visibility => intEnum<AssetVisibility>()();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -51,6 +53,7 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
|
||||||
width: width,
|
width: width,
|
||||||
thumbHash: thumbHash,
|
thumbHash: thumbHash,
|
||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
|
livePhotoVideoId: livePhotoVideoId,
|
||||||
localId: null,
|
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/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/memory.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/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/memory_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/partner.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.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.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_album_user.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.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/stack.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
|
@ -134,6 +134,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||||
thumbHash: Value(asset.thumbhash),
|
thumbHash: Value(asset.thumbhash),
|
||||||
deletedAt: Value(asset.deletedAt),
|
deletedAt: Value(asset.deletedAt),
|
||||||
visibility: Value(asset.visibility.toAssetVisibility()),
|
visibility: Value(asset.visibility.toAssetVisibility()),
|
||||||
|
livePhotoVideoId: Value(asset.livePhotoVideoId),
|
||||||
);
|
);
|
||||||
|
|
||||||
batch.insert(
|
batch.insert(
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||||
height: row.height,
|
height: row.height,
|
||||||
isFavorite: row.isFavorite,
|
isFavorite: row.isFavorite,
|
||||||
durationInSeconds: row.durationInSeconds,
|
durationInSeconds: row.durationInSeconds,
|
||||||
|
livePhotoVideoId: row.livePhotoVideoId,
|
||||||
)
|
)
|
||||||
: LocalAsset(
|
: LocalAsset(
|
||||||
id: row.localId!,
|
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/asset_viewer/video_viewer.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.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/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_controls_provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/video_player_value_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';
|
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) {
|
void _onAssetChanged(int index) {
|
||||||
final asset = ref.read(timelineServiceProvider).getAsset(index);
|
final asset = ref.read(timelineServiceProvider).getAsset(index);
|
||||||
ref.read(currentAssetNotifier.notifier).setAsset(asset);
|
ref.read(currentAssetNotifier.notifier).setAsset(asset);
|
||||||
if (asset.isVideo) {
|
if (asset.isVideo || asset.isMotionPhoto) {
|
||||||
ref.read(videoPlaybackValueProvider.notifier).reset();
|
ref.read(videoPlaybackValueProvider.notifier).reset();
|
||||||
ref.read(videoPlayerControlsProvider.notifier).pause();
|
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) {
|
PhotoViewGalleryPageOptions _assetBuilder(BuildContext ctx, int index) {
|
||||||
scaffoldContext ??= ctx;
|
scaffoldContext ??= ctx;
|
||||||
final asset = ref.read(timelineServiceProvider).getAsset(index);
|
final asset = ref.read(timelineServiceProvider).getAsset(index);
|
||||||
|
final isPlayingMotionVideo = ref.read(isPlayingMotionVideoProvider);
|
||||||
|
|
||||||
if (asset.isImage) {
|
if (asset.isImage && !isPlayingMotionVideo) {
|
||||||
return _imageBuilder(ctx, asset);
|
return _imageBuilder(ctx, asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -500,6 +506,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||||
onDragUpdate: _onDragUpdate,
|
onDragUpdate: _onDragUpdate,
|
||||||
onDragEnd: _onDragEnd,
|
onDragEnd: _onDragEnd,
|
||||||
onTapDown: _onTapDown,
|
onTapDown: _onTapDown,
|
||||||
|
onLongPressStart: asset.isMotionPhoto ? _onLongPress : null,
|
||||||
errorBuilder: (_, __, ___) => Container(
|
errorBuilder: (_, __, ___) => Container(
|
||||||
width: ctx.width,
|
width: ctx.width,
|
||||||
height: ctx.height,
|
height: ctx.height,
|
||||||
|
|
@ -561,6 +568,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||||
// Using multiple selectors to avoid unnecessary rebuilds for other state changes
|
// Using multiple selectors to avoid unnecessary rebuilds for other state changes
|
||||||
ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet));
|
ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet));
|
||||||
ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity));
|
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.
|
// 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
|
// 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/domain/utils/event_stream.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.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/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/action_buttons/unfavorite_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.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';
|
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,
|
source: ActionSource.viewer,
|
||||||
menuItem: true,
|
menuItem: true,
|
||||||
),
|
),
|
||||||
|
if (asset.isMotionPhoto) const MotionPhotoActionButton(menuItem: true),
|
||||||
const _KebabMenu(),
|
const _KebabMenu(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -270,10 +270,7 @@ class NativeVideoViewer extends HookConsumerWidget {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (videoController.playbackInfo?.status == PlaybackStatus.stopped &&
|
if (videoController.playbackInfo?.status == PlaybackStatus.stopped) {
|
||||||
!ref
|
|
||||||
.read(appSettingsServiceProvider)
|
|
||||||
.getSetting<bool>(AppSettingsEnum.loopVideo)) {
|
|
||||||
ref.read(isPlayingMotionVideoProvider.notifier).playing = false;
|
ref.read(isPlayingMotionVideoProvider.notifier).playing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -310,7 +307,7 @@ class NativeVideoViewer extends HookConsumerWidget {
|
||||||
final loopVideo = ref
|
final loopVideo = ref
|
||||||
.read(appSettingsServiceProvider)
|
.read(appSettingsServiceProvider)
|
||||||
.getSetting<bool>(AppSettingsEnum.loopVideo);
|
.getSetting<bool>(AppSettingsEnum.loopVideo);
|
||||||
nc.setLoop(loopVideo);
|
nc.setLoop(!asset.isMotionPhoto && loopVideo);
|
||||||
|
|
||||||
controller.value = nc;
|
controller.value = nc;
|
||||||
Timer(const Duration(milliseconds: 200), checkIfBuffering);
|
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.model.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/segment_builder.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/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/haptic_feedback.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.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);
|
ref.read(multiSelectProvider.notifier).toggleAssetSelection(asset);
|
||||||
} else {
|
} else {
|
||||||
await ref.read(timelineServiceProvider).loadAssets(assetIndex, 1);
|
await ref.read(timelineServiceProvider).loadAssets(assetIndex, 1);
|
||||||
|
ref.read(isPlayingMotionVideoProvider.notifier).playing = false;
|
||||||
ctx.pushRoute(
|
ctx.pushRoute(
|
||||||
AssetViewerRoute(
|
AssetViewerRoute(
|
||||||
initialIndex: assetIndex,
|
initialIndex: assetIndex,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue