1
0
mirror of https://github.com/immich-app/immich.git synced 2025-07-17 15:17:51 +02: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:
shenlong
2025-07-15 00:53:24 +05:30
committed by GitHub
parent 805ec3e351
commit 9abb95d34a
14 changed files with 119 additions and 10 deletions

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

@ -18,7 +18,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
final generatedlimit = $write(limit, startIndex: $arrayStartIndex); final generatedlimit = $write(limit, startIndex: $arrayStartIndex);
$arrayStartIndex += generatedlimit.amountOfVariables; $arrayStartIndex += generatedlimit.amountOfVariables;
return customSelect( return customSelect(
'SELECT * FROM (SELECT rae.id AS remote_id, lae.id AS local_id, rae.name, rae.type, rae.created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, 0 AS orientation FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar1) UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, lae.orientation FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) ORDER BY created_at DESC ${generatedlimit.sql}', 'SELECT * FROM (SELECT rae.id AS remote_id, lae.id AS local_id, rae.name, rae.type, rae.created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar1) UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) ORDER BY created_at DESC ${generatedlimit.sql}',
variables: [ variables: [
for (var $ in var1) i0.Variable<String>($), for (var $ in var1) i0.Variable<String>($),
...generatedlimit.introducedVariables ...generatedlimit.introducedVariables
@ -42,6 +42,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
thumbHash: row.readNullable<String>('thumb_hash'), thumbHash: row.readNullable<String>('thumb_hash'),
checksum: row.readNullable<String>('checksum'), checksum: row.readNullable<String>('checksum'),
ownerId: row.readNullable<String>('owner_id'), ownerId: row.readNullable<String>('owner_id'),
livePhotoVideoId: row.readNullable<String>('live_photo_video_id'),
orientation: row.read<int>('orientation'), orientation: row.read<int>('orientation'),
)); ));
} }
@ -88,6 +89,7 @@ class MergedAssetResult {
final String? thumbHash; final String? thumbHash;
final String? checksum; final String? checksum;
final String? ownerId; final String? ownerId;
final String? livePhotoVideoId;
final int orientation; final int orientation;
MergedAssetResult({ MergedAssetResult({
this.remoteId, this.remoteId,
@ -103,6 +105,7 @@ class MergedAssetResult {
this.thumbHash, this.thumbHash,
this.checksum, this.checksum,
this.ownerId, this.ownerId,
this.livePhotoVideoId,
required this.orientation, required this.orientation,
}); });
} }

View File

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

View File

@ -27,6 +27,7 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder
i0.Value<DateTime?> localDateTime, i0.Value<DateTime?> localDateTime,
i0.Value<String?> thumbHash, i0.Value<String?> thumbHash,
i0.Value<DateTime?> deletedAt, i0.Value<DateTime?> deletedAt,
i0.Value<String?> livePhotoVideoId,
required i2.AssetVisibility visibility, required i2.AssetVisibility visibility,
}); });
typedef $$RemoteAssetEntityTableUpdateCompanionBuilder typedef $$RemoteAssetEntityTableUpdateCompanionBuilder
@ -45,6 +46,7 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder
i0.Value<DateTime?> localDateTime, i0.Value<DateTime?> localDateTime,
i0.Value<String?> thumbHash, i0.Value<String?> thumbHash,
i0.Value<DateTime?> deletedAt, i0.Value<DateTime?> deletedAt,
i0.Value<String?> livePhotoVideoId,
i0.Value<i2.AssetVisibility> visibility, i0.Value<i2.AssetVisibility> visibility,
}); });
@ -134,6 +136,10 @@ class $$RemoteAssetEntityTableFilterComposer
i0.ColumnFilters<DateTime> get deletedAt => $composableBuilder( i0.ColumnFilters<DateTime> get deletedAt => $composableBuilder(
column: $table.deletedAt, builder: (column) => i0.ColumnFilters(column)); column: $table.deletedAt, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get livePhotoVideoId => $composableBuilder(
column: $table.livePhotoVideoId,
builder: (column) => i0.ColumnFilters(column));
i0.ColumnWithTypeConverterFilters<i2.AssetVisibility, i2.AssetVisibility, int> i0.ColumnWithTypeConverterFilters<i2.AssetVisibility, i2.AssetVisibility, int>
get visibility => $composableBuilder( get visibility => $composableBuilder(
column: $table.visibility, column: $table.visibility,
@ -217,6 +223,10 @@ class $$RemoteAssetEntityTableOrderingComposer
column: $table.deletedAt, column: $table.deletedAt,
builder: (column) => i0.ColumnOrderings(column)); builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get livePhotoVideoId => $composableBuilder(
column: $table.livePhotoVideoId,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<int> get visibility => $composableBuilder( i0.ColumnOrderings<int> get visibility => $composableBuilder(
column: $table.visibility, column: $table.visibility,
builder: (column) => i0.ColumnOrderings(column)); builder: (column) => i0.ColumnOrderings(column));
@ -292,6 +302,9 @@ class $$RemoteAssetEntityTableAnnotationComposer
i0.GeneratedColumn<DateTime> get deletedAt => i0.GeneratedColumn<DateTime> get deletedAt =>
$composableBuilder(column: $table.deletedAt, builder: (column) => column); $composableBuilder(column: $table.deletedAt, builder: (column) => column);
i0.GeneratedColumn<String> get livePhotoVideoId => $composableBuilder(
column: $table.livePhotoVideoId, builder: (column) => column);
i0.GeneratedColumnWithTypeConverter<i2.AssetVisibility, int> get visibility => i0.GeneratedColumnWithTypeConverter<i2.AssetVisibility, int> get visibility =>
$composableBuilder( $composableBuilder(
column: $table.visibility, builder: (column) => column); column: $table.visibility, builder: (column) => column);
@ -358,6 +371,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
i0.Value<DateTime?> localDateTime = const i0.Value.absent(), i0.Value<DateTime?> localDateTime = const i0.Value.absent(),
i0.Value<String?> thumbHash = const i0.Value.absent(), i0.Value<String?> thumbHash = const i0.Value.absent(),
i0.Value<DateTime?> deletedAt = const i0.Value.absent(), i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
i0.Value<i2.AssetVisibility> visibility = const i0.Value.absent(), i0.Value<i2.AssetVisibility> visibility = const i0.Value.absent(),
}) => }) =>
i1.RemoteAssetEntityCompanion( i1.RemoteAssetEntityCompanion(
@ -375,6 +389,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
localDateTime: localDateTime, localDateTime: localDateTime,
thumbHash: thumbHash, thumbHash: thumbHash,
deletedAt: deletedAt, deletedAt: deletedAt,
livePhotoVideoId: livePhotoVideoId,
visibility: visibility, visibility: visibility,
), ),
createCompanionCallback: ({ createCompanionCallback: ({
@ -392,6 +407,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
i0.Value<DateTime?> localDateTime = const i0.Value.absent(), i0.Value<DateTime?> localDateTime = const i0.Value.absent(),
i0.Value<String?> thumbHash = const i0.Value.absent(), i0.Value<String?> thumbHash = const i0.Value.absent(),
i0.Value<DateTime?> deletedAt = const i0.Value.absent(), i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
required i2.AssetVisibility visibility, required i2.AssetVisibility visibility,
}) => }) =>
i1.RemoteAssetEntityCompanion.insert( i1.RemoteAssetEntityCompanion.insert(
@ -409,6 +425,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
localDateTime: localDateTime, localDateTime: localDateTime,
thumbHash: thumbHash, thumbHash: thumbHash,
deletedAt: deletedAt, deletedAt: deletedAt,
livePhotoVideoId: livePhotoVideoId,
visibility: visibility, visibility: visibility,
), ),
withReferenceMapper: (p0) => p0 withReferenceMapper: (p0) => p0
@ -573,6 +590,12 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
late final i0.GeneratedColumn<DateTime> deletedAt = late final i0.GeneratedColumn<DateTime> deletedAt =
i0.GeneratedColumn<DateTime>('deleted_at', aliasedName, true, i0.GeneratedColumn<DateTime>('deleted_at', aliasedName, true,
type: i0.DriftSqlType.dateTime, requiredDuringInsert: false); type: i0.DriftSqlType.dateTime, requiredDuringInsert: false);
static const i0.VerificationMeta _livePhotoVideoIdMeta =
const i0.VerificationMeta('livePhotoVideoId');
@override
late final i0.GeneratedColumn<String> livePhotoVideoId =
i0.GeneratedColumn<String>('live_photo_video_id', aliasedName, true,
type: i0.DriftSqlType.string, requiredDuringInsert: false);
@override @override
late final i0.GeneratedColumnWithTypeConverter<i2.AssetVisibility, int> late final i0.GeneratedColumnWithTypeConverter<i2.AssetVisibility, int>
visibility = i0.GeneratedColumn<int>('visibility', aliasedName, false, visibility = i0.GeneratedColumn<int>('visibility', aliasedName, false,
@ -595,6 +618,7 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
localDateTime, localDateTime,
thumbHash, thumbHash,
deletedAt, deletedAt,
livePhotoVideoId,
visibility visibility
]; ];
@override @override
@ -673,6 +697,12 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
context.handle(_deletedAtMeta, context.handle(_deletedAtMeta,
deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta)); deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta));
} }
if (data.containsKey('live_photo_video_id')) {
context.handle(
_livePhotoVideoIdMeta,
livePhotoVideoId.isAcceptableOrUnknown(
data['live_photo_video_id']!, _livePhotoVideoIdMeta));
}
return context; return context;
} }
@ -712,6 +742,9 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
.read(i0.DriftSqlType.string, data['${effectivePrefix}thumb_hash']), .read(i0.DriftSqlType.string, data['${effectivePrefix}thumb_hash']),
deletedAt: attachedDatabase.typeMapping deletedAt: attachedDatabase.typeMapping
.read(i0.DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), .read(i0.DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']),
livePhotoVideoId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}live_photo_video_id']),
visibility: i1.$RemoteAssetEntityTable.$convertervisibility.fromSql( visibility: i1.$RemoteAssetEntityTable.$convertervisibility.fromSql(
attachedDatabase.typeMapping.read( attachedDatabase.typeMapping.read(
i0.DriftSqlType.int, data['${effectivePrefix}visibility'])!), i0.DriftSqlType.int, data['${effectivePrefix}visibility'])!),
@ -750,6 +783,7 @@ class RemoteAssetEntityData extends i0.DataClass
final DateTime? localDateTime; final DateTime? localDateTime;
final String? thumbHash; final String? thumbHash;
final DateTime? deletedAt; final DateTime? deletedAt;
final String? livePhotoVideoId;
final i2.AssetVisibility visibility; final i2.AssetVisibility visibility;
const RemoteAssetEntityData( const RemoteAssetEntityData(
{required this.name, {required this.name,
@ -766,6 +800,7 @@ class RemoteAssetEntityData extends i0.DataClass
this.localDateTime, this.localDateTime,
this.thumbHash, this.thumbHash,
this.deletedAt, this.deletedAt,
this.livePhotoVideoId,
required this.visibility}); required this.visibility});
@override @override
Map<String, i0.Expression> toColumns(bool nullToAbsent) { Map<String, i0.Expression> toColumns(bool nullToAbsent) {
@ -799,6 +834,9 @@ class RemoteAssetEntityData extends i0.DataClass
if (!nullToAbsent || deletedAt != null) { if (!nullToAbsent || deletedAt != null) {
map['deleted_at'] = i0.Variable<DateTime>(deletedAt); map['deleted_at'] = i0.Variable<DateTime>(deletedAt);
} }
if (!nullToAbsent || livePhotoVideoId != null) {
map['live_photo_video_id'] = i0.Variable<String>(livePhotoVideoId);
}
{ {
map['visibility'] = i0.Variable<int>( map['visibility'] = i0.Variable<int>(
i1.$RemoteAssetEntityTable.$convertervisibility.toSql(visibility)); i1.$RemoteAssetEntityTable.$convertervisibility.toSql(visibility));
@ -825,6 +863,7 @@ class RemoteAssetEntityData extends i0.DataClass
localDateTime: serializer.fromJson<DateTime?>(json['localDateTime']), localDateTime: serializer.fromJson<DateTime?>(json['localDateTime']),
thumbHash: serializer.fromJson<String?>(json['thumbHash']), thumbHash: serializer.fromJson<String?>(json['thumbHash']),
deletedAt: serializer.fromJson<DateTime?>(json['deletedAt']), deletedAt: serializer.fromJson<DateTime?>(json['deletedAt']),
livePhotoVideoId: serializer.fromJson<String?>(json['livePhotoVideoId']),
visibility: i1.$RemoteAssetEntityTable.$convertervisibility visibility: i1.$RemoteAssetEntityTable.$convertervisibility
.fromJson(serializer.fromJson<int>(json['visibility'])), .fromJson(serializer.fromJson<int>(json['visibility'])),
); );
@ -848,6 +887,7 @@ class RemoteAssetEntityData extends i0.DataClass
'localDateTime': serializer.toJson<DateTime?>(localDateTime), 'localDateTime': serializer.toJson<DateTime?>(localDateTime),
'thumbHash': serializer.toJson<String?>(thumbHash), 'thumbHash': serializer.toJson<String?>(thumbHash),
'deletedAt': serializer.toJson<DateTime?>(deletedAt), 'deletedAt': serializer.toJson<DateTime?>(deletedAt),
'livePhotoVideoId': serializer.toJson<String?>(livePhotoVideoId),
'visibility': serializer.toJson<int>( 'visibility': serializer.toJson<int>(
i1.$RemoteAssetEntityTable.$convertervisibility.toJson(visibility)), i1.$RemoteAssetEntityTable.$convertervisibility.toJson(visibility)),
}; };
@ -868,6 +908,7 @@ class RemoteAssetEntityData extends i0.DataClass
i0.Value<DateTime?> localDateTime = const i0.Value.absent(), i0.Value<DateTime?> localDateTime = const i0.Value.absent(),
i0.Value<String?> thumbHash = const i0.Value.absent(), i0.Value<String?> thumbHash = const i0.Value.absent(),
i0.Value<DateTime?> deletedAt = const i0.Value.absent(), i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
i2.AssetVisibility? visibility}) => i2.AssetVisibility? visibility}) =>
i1.RemoteAssetEntityData( i1.RemoteAssetEntityData(
name: name ?? this.name, name: name ?? this.name,
@ -887,6 +928,9 @@ class RemoteAssetEntityData extends i0.DataClass
localDateTime.present ? localDateTime.value : this.localDateTime, localDateTime.present ? localDateTime.value : this.localDateTime,
thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash,
deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt,
livePhotoVideoId: livePhotoVideoId.present
? livePhotoVideoId.value
: this.livePhotoVideoId,
visibility: visibility ?? this.visibility, visibility: visibility ?? this.visibility,
); );
RemoteAssetEntityData copyWithCompanion(i1.RemoteAssetEntityCompanion data) { RemoteAssetEntityData copyWithCompanion(i1.RemoteAssetEntityCompanion data) {
@ -910,6 +954,9 @@ class RemoteAssetEntityData extends i0.DataClass
: this.localDateTime, : this.localDateTime,
thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash,
deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt,
livePhotoVideoId: data.livePhotoVideoId.present
? data.livePhotoVideoId.value
: this.livePhotoVideoId,
visibility: visibility:
data.visibility.present ? data.visibility.value : this.visibility, data.visibility.present ? data.visibility.value : this.visibility,
); );
@ -932,6 +979,7 @@ class RemoteAssetEntityData extends i0.DataClass
..write('localDateTime: $localDateTime, ') ..write('localDateTime: $localDateTime, ')
..write('thumbHash: $thumbHash, ') ..write('thumbHash: $thumbHash, ')
..write('deletedAt: $deletedAt, ') ..write('deletedAt: $deletedAt, ')
..write('livePhotoVideoId: $livePhotoVideoId, ')
..write('visibility: $visibility') ..write('visibility: $visibility')
..write(')')) ..write(')'))
.toString(); .toString();
@ -953,6 +1001,7 @@ class RemoteAssetEntityData extends i0.DataClass
localDateTime, localDateTime,
thumbHash, thumbHash,
deletedAt, deletedAt,
livePhotoVideoId,
visibility); visibility);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@ -972,6 +1021,7 @@ class RemoteAssetEntityData extends i0.DataClass
other.localDateTime == this.localDateTime && other.localDateTime == this.localDateTime &&
other.thumbHash == this.thumbHash && other.thumbHash == this.thumbHash &&
other.deletedAt == this.deletedAt && other.deletedAt == this.deletedAt &&
other.livePhotoVideoId == this.livePhotoVideoId &&
other.visibility == this.visibility); other.visibility == this.visibility);
} }
@ -991,6 +1041,7 @@ class RemoteAssetEntityCompanion
final i0.Value<DateTime?> localDateTime; final i0.Value<DateTime?> localDateTime;
final i0.Value<String?> thumbHash; final i0.Value<String?> thumbHash;
final i0.Value<DateTime?> deletedAt; final i0.Value<DateTime?> deletedAt;
final i0.Value<String?> livePhotoVideoId;
final i0.Value<i2.AssetVisibility> visibility; final i0.Value<i2.AssetVisibility> visibility;
const RemoteAssetEntityCompanion({ const RemoteAssetEntityCompanion({
this.name = const i0.Value.absent(), this.name = const i0.Value.absent(),
@ -1007,6 +1058,7 @@ class RemoteAssetEntityCompanion
this.localDateTime = const i0.Value.absent(), this.localDateTime = const i0.Value.absent(),
this.thumbHash = const i0.Value.absent(), this.thumbHash = const i0.Value.absent(),
this.deletedAt = const i0.Value.absent(), this.deletedAt = const i0.Value.absent(),
this.livePhotoVideoId = const i0.Value.absent(),
this.visibility = const i0.Value.absent(), this.visibility = const i0.Value.absent(),
}); });
RemoteAssetEntityCompanion.insert({ RemoteAssetEntityCompanion.insert({
@ -1024,6 +1076,7 @@ class RemoteAssetEntityCompanion
this.localDateTime = const i0.Value.absent(), this.localDateTime = const i0.Value.absent(),
this.thumbHash = const i0.Value.absent(), this.thumbHash = const i0.Value.absent(),
this.deletedAt = const i0.Value.absent(), this.deletedAt = const i0.Value.absent(),
this.livePhotoVideoId = const i0.Value.absent(),
required i2.AssetVisibility visibility, required i2.AssetVisibility visibility,
}) : name = i0.Value(name), }) : name = i0.Value(name),
type = i0.Value(type), type = i0.Value(type),
@ -1046,6 +1099,7 @@ class RemoteAssetEntityCompanion
i0.Expression<DateTime>? localDateTime, i0.Expression<DateTime>? localDateTime,
i0.Expression<String>? thumbHash, i0.Expression<String>? thumbHash,
i0.Expression<DateTime>? deletedAt, i0.Expression<DateTime>? deletedAt,
i0.Expression<String>? livePhotoVideoId,
i0.Expression<int>? visibility, i0.Expression<int>? visibility,
}) { }) {
return i0.RawValuesInsertable({ return i0.RawValuesInsertable({
@ -1063,6 +1117,7 @@ class RemoteAssetEntityCompanion
if (localDateTime != null) 'local_date_time': localDateTime, if (localDateTime != null) 'local_date_time': localDateTime,
if (thumbHash != null) 'thumb_hash': thumbHash, if (thumbHash != null) 'thumb_hash': thumbHash,
if (deletedAt != null) 'deleted_at': deletedAt, if (deletedAt != null) 'deleted_at': deletedAt,
if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId,
if (visibility != null) 'visibility': visibility, if (visibility != null) 'visibility': visibility,
}); });
} }
@ -1082,6 +1137,7 @@ class RemoteAssetEntityCompanion
i0.Value<DateTime?>? localDateTime, i0.Value<DateTime?>? localDateTime,
i0.Value<String?>? thumbHash, i0.Value<String?>? thumbHash,
i0.Value<DateTime?>? deletedAt, i0.Value<DateTime?>? deletedAt,
i0.Value<String?>? livePhotoVideoId,
i0.Value<i2.AssetVisibility>? visibility}) { i0.Value<i2.AssetVisibility>? visibility}) {
return i1.RemoteAssetEntityCompanion( return i1.RemoteAssetEntityCompanion(
name: name ?? this.name, name: name ?? this.name,
@ -1098,6 +1154,7 @@ class RemoteAssetEntityCompanion
localDateTime: localDateTime ?? this.localDateTime, localDateTime: localDateTime ?? this.localDateTime,
thumbHash: thumbHash ?? this.thumbHash, thumbHash: thumbHash ?? this.thumbHash,
deletedAt: deletedAt ?? this.deletedAt, deletedAt: deletedAt ?? this.deletedAt,
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
visibility: visibility ?? this.visibility, visibility: visibility ?? this.visibility,
); );
} }
@ -1148,6 +1205,9 @@ class RemoteAssetEntityCompanion
if (deletedAt.present) { if (deletedAt.present) {
map['deleted_at'] = i0.Variable<DateTime>(deletedAt.value); map['deleted_at'] = i0.Variable<DateTime>(deletedAt.value);
} }
if (livePhotoVideoId.present) {
map['live_photo_video_id'] = i0.Variable<String>(livePhotoVideoId.value);
}
if (visibility.present) { if (visibility.present) {
map['visibility'] = i0.Variable<int>(i1 map['visibility'] = i0.Variable<int>(i1
.$RemoteAssetEntityTable.$convertervisibility .$RemoteAssetEntityTable.$convertervisibility
@ -1173,6 +1233,7 @@ class RemoteAssetEntityCompanion
..write('localDateTime: $localDateTime, ') ..write('localDateTime: $localDateTime, ')
..write('thumbHash: $thumbHash, ') ..write('thumbHash: $thumbHash, ')
..write('deletedAt: $deletedAt, ') ..write('deletedAt: $deletedAt, ')
..write('livePhotoVideoId: $livePhotoVideoId, ')
..write('visibility: $visibility') ..write('visibility: $visibility')
..write(')')) ..write(')'))
.toString(); .toString();

View File

@ -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(

View File

@ -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!,

View File

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

View File

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

View File

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

View File

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

View File

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