mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
Include trashed items in getMediaChanges
Process trashed items delta during incremental sync
This commit is contained in:
parent
5ddb6cd2e1
commit
55fe480cc1
9 changed files with 110 additions and 36 deletions
|
|
@ -90,6 +90,7 @@ data class PlatformAsset (
|
||||||
val durationInSeconds: Long,
|
val durationInSeconds: Long,
|
||||||
val orientation: Long,
|
val orientation: Long,
|
||||||
val isFavorite: Boolean,
|
val isFavorite: Boolean,
|
||||||
|
val isTrashed: Boolean,
|
||||||
val size: Long? = null
|
val size: Long? = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
|
@ -105,8 +106,9 @@ data class PlatformAsset (
|
||||||
val durationInSeconds = pigeonVar_list[7] as Long
|
val durationInSeconds = pigeonVar_list[7] as Long
|
||||||
val orientation = pigeonVar_list[8] as Long
|
val orientation = pigeonVar_list[8] as Long
|
||||||
val isFavorite = pigeonVar_list[9] as Boolean
|
val isFavorite = pigeonVar_list[9] as Boolean
|
||||||
val size = pigeonVar_list[10] as Long?
|
val isTrashed = pigeonVar_list[10] as Boolean
|
||||||
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite, size)
|
val size = pigeonVar_list[11] as Long?
|
||||||
|
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite, isTrashed, size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun toList(): List<Any?> {
|
fun toList(): List<Any?> {
|
||||||
|
|
@ -121,6 +123,7 @@ data class PlatformAsset (
|
||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
orientation,
|
orientation,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
|
isTrashed,
|
||||||
size,
|
size,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,6 @@ class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), Na
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresExtension(extension = Build.VERSION_CODES.R, version = 1)
|
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
|
||||||
override fun getTrashedAssetsForAlbum(
|
override fun getTrashedAssetsForAlbum(
|
||||||
albumId: String,
|
albumId: String,
|
||||||
updatedTimeCond: Long?
|
updatedTimeCond: Long?
|
||||||
|
|
@ -155,14 +153,22 @@ class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), Na
|
||||||
storedGen.toString()
|
storedGen.toString()
|
||||||
)
|
)
|
||||||
|
|
||||||
getAssets(getCursor(volume, selection, selectionArgs)).forEach {
|
val uri = MediaStore.Files.getContentUri(volume)
|
||||||
when (it) {
|
val queryArgs = Bundle().apply {
|
||||||
is AssetResult.ValidAsset -> {
|
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection)
|
||||||
changed.add(it.asset)
|
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, selectionArgs)
|
||||||
assetAlbums[it.asset.id] = listOf(it.albumId)
|
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE)
|
||||||
}
|
}
|
||||||
|
|
||||||
is AssetResult.InvalidAsset -> deleted.add(it.assetId)
|
ctx.contentResolver.query(uri, ASSET_PROJECTION, queryArgs, null).use { cursor ->
|
||||||
|
getAssets(cursor).forEach {
|
||||||
|
when (it) {
|
||||||
|
is AssetResult.ValidAsset -> {
|
||||||
|
changed.add(it.asset)
|
||||||
|
assetAlbums[it.asset.id] = listOf(it.albumId)
|
||||||
|
}
|
||||||
|
is AssetResult.InvalidAsset -> deleted.add(it.assetId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,8 @@ open class NativeSyncApiImplBase(context: Context) {
|
||||||
// IS_FAVORITE is only available on Android 11 and above
|
// IS_FAVORITE is only available on Android 11 and above
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
add(MediaStore.MediaColumns.IS_FAVORITE)
|
add(MediaStore.MediaColumns.IS_FAVORITE)
|
||||||
|
// IS_TRASHED available on Android 11+
|
||||||
|
add(MediaStore.MediaColumns.IS_TRASHED)
|
||||||
}
|
}
|
||||||
add(MediaStore.MediaColumns.SIZE)
|
add(MediaStore.MediaColumns.SIZE)
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
|
|
@ -99,6 +101,7 @@ open class NativeSyncApiImplBase(context: Context) {
|
||||||
val orientationColumn =
|
val orientationColumn =
|
||||||
c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION)
|
c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION)
|
||||||
val favoriteColumn = c.getColumnIndex(MediaStore.MediaColumns.IS_FAVORITE)
|
val favoriteColumn = c.getColumnIndex(MediaStore.MediaColumns.IS_FAVORITE)
|
||||||
|
val trashedColumn = c.getColumnIndex(MediaStore.MediaColumns.IS_TRASHED)
|
||||||
val sizeColumn = c.getColumnIndex(MediaStore.MediaColumns.SIZE)
|
val sizeColumn = c.getColumnIndex(MediaStore.MediaColumns.SIZE)
|
||||||
|
|
||||||
while (c.moveToNext()) {
|
while (c.moveToNext()) {
|
||||||
|
|
@ -129,6 +132,7 @@ open class NativeSyncApiImplBase(context: Context) {
|
||||||
val bucketId = c.getString(bucketIdColumn)
|
val bucketId = c.getString(bucketIdColumn)
|
||||||
val orientation = c.getInt(orientationColumn)
|
val orientation = c.getInt(orientationColumn)
|
||||||
val isFavorite = if (favoriteColumn == -1) false else c.getInt(favoriteColumn) != 0
|
val isFavorite = if (favoriteColumn == -1) false else c.getInt(favoriteColumn) != 0
|
||||||
|
val isTrashed = if (trashedColumn == -1) false else c.getInt(trashedColumn) != 0
|
||||||
val size = c.getLong(sizeColumn)
|
val size = c.getLong(sizeColumn)
|
||||||
val asset = PlatformAsset(
|
val asset = PlatformAsset(
|
||||||
id,
|
id,
|
||||||
|
|
@ -141,6 +145,7 @@ open class NativeSyncApiImplBase(context: Context) {
|
||||||
duration,
|
duration,
|
||||||
orientation.toLong(),
|
orientation.toLong(),
|
||||||
isFavorite,
|
isFavorite,
|
||||||
|
isTrashed,
|
||||||
size
|
size
|
||||||
)
|
)
|
||||||
yield(AssetResult.ValidAsset(asset, bucketId))
|
yield(AssetResult.ValidAsset(asset, bucketId))
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,7 @@ struct PlatformAsset: Hashable {
|
||||||
var durationInSeconds: Int64
|
var durationInSeconds: Int64
|
||||||
var orientation: Int64
|
var orientation: Int64
|
||||||
var isFavorite: Bool
|
var isFavorite: Bool
|
||||||
|
var isTrashed: Bool
|
||||||
var size: Int64? = nil
|
var size: Int64? = nil
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -155,7 +156,8 @@ struct PlatformAsset: Hashable {
|
||||||
let durationInSeconds = pigeonVar_list[7] as! Int64
|
let durationInSeconds = pigeonVar_list[7] as! Int64
|
||||||
let orientation = pigeonVar_list[8] as! Int64
|
let orientation = pigeonVar_list[8] as! Int64
|
||||||
let isFavorite = pigeonVar_list[9] as! Bool
|
let isFavorite = pigeonVar_list[9] as! Bool
|
||||||
let size: Int64? = nilOrValue(pigeonVar_list[10])
|
let isTrashed = pigeonVar_list[10] as! Bool
|
||||||
|
let size: Int64? = nilOrValue(pigeonVar_list[11])
|
||||||
|
|
||||||
return PlatformAsset(
|
return PlatformAsset(
|
||||||
id: id,
|
id: id,
|
||||||
|
|
@ -168,6 +170,7 @@ struct PlatformAsset: Hashable {
|
||||||
durationInSeconds: durationInSeconds,
|
durationInSeconds: durationInSeconds,
|
||||||
orientation: orientation,
|
orientation: orientation,
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
|
isTrashed: isTrashed,
|
||||||
size: size
|
size: size
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -183,6 +186,7 @@ struct PlatformAsset: Hashable {
|
||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
orientation,
|
orientation,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
|
isTrashed,
|
||||||
size,
|
size,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_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/extensions/platform_extensions.dart';
|
|
||||||
import 'package:immich_mobile/domain/services/trash_sync.service.dart';
|
import 'package:immich_mobile/domain/services/trash_sync.service.dart';
|
||||||
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||||
import 'package:immich_mobile/utils/datetime_helpers.dart';
|
import 'package:immich_mobile/utils/datetime_helpers.dart';
|
||||||
|
|
@ -40,13 +40,14 @@ class LocalSyncService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_log.fine("Delta updated: ${delta.updates.length}");
|
final updates = delta.updates.where((e) => !e.isTrashed);
|
||||||
|
_log.fine("Delta updated assets: ${updates.length}");
|
||||||
_log.fine("Delta deleted: ${delta.deletes.length}");
|
_log.fine("Delta deleted: ${delta.deletes.length}");
|
||||||
|
|
||||||
final deviceAlbums = await _nativeSyncApi.getAlbums();
|
final deviceAlbums = await _nativeSyncApi.getAlbums();
|
||||||
await _localAlbumRepository.updateAll(deviceAlbums.toLocalAlbums());
|
await _localAlbumRepository.updateAll(deviceAlbums.toLocalAlbums());
|
||||||
await _localAlbumRepository.processDelta(
|
await _localAlbumRepository.processDelta(
|
||||||
updates: delta.updates.toLocalAssets(),
|
updates: updates.toLocalAssets(),
|
||||||
deletes: delta.deletes,
|
deletes: delta.deletes,
|
||||||
assetAlbums: delta.assetAlbums,
|
assetAlbums: delta.assetAlbums,
|
||||||
);
|
);
|
||||||
|
|
@ -76,8 +77,8 @@ class LocalSyncService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_trashSyncService.isAutoSyncMode) {
|
if (_trashSyncService.isAutoSyncMode) {
|
||||||
// On Android we need to sync trashed assets
|
_log.fine("Delta updated trashed: ${delta.updates.length - updates.length}");
|
||||||
await _trashSyncService.updateLocalTrashFromDevice();
|
await _trashSyncService.applyTrashDelta(delta);
|
||||||
}
|
}
|
||||||
await _nativeSyncApi.checkpointSync();
|
await _nativeSyncApi.checkpointSync();
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
|
|
@ -104,7 +105,7 @@ class LocalSyncService {
|
||||||
onlySecond: addAlbum,
|
onlySecond: addAlbum,
|
||||||
);
|
);
|
||||||
if (_trashSyncService.isAutoSyncMode) {
|
if (_trashSyncService.isAutoSyncMode) {
|
||||||
await _trashSyncService.updateLocalTrashFromDevice();
|
await _trashSyncService.syncDeviceTrashSnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _nativeSyncApi.checkpointSync();
|
await _nativeSyncApi.checkpointSync();
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ class TrashSyncService {
|
||||||
Future<Iterable<TrashedAsset>> getAssetsToHash(String albumId) async =>
|
Future<Iterable<TrashedAsset>> getAssetsToHash(String albumId) async =>
|
||||||
_trashedLocalAssetRepository.getToHash(albumId);
|
_trashedLocalAssetRepository.getToHash(albumId);
|
||||||
|
|
||||||
Future<void> updateLocalTrashFromDevice() async {
|
Future<void> syncDeviceTrashSnapshot() async {
|
||||||
final backupAlbums = await _localAlbumRepository.getBackupAlbums();
|
final backupAlbums = await _localAlbumRepository.getBackupAlbums();
|
||||||
if (backupAlbums.isEmpty) {
|
if (backupAlbums.isEmpty) {
|
||||||
_logger.info("No backup albums found");
|
_logger.info("No backup albums found");
|
||||||
|
|
@ -63,6 +63,24 @@ class TrashSyncService {
|
||||||
await applyRemoteRestoreToLocal();
|
await applyRemoteRestoreToLocal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> applyTrashDelta(SyncDelta delta) async {
|
||||||
|
final trashUpdates = delta.updates.where((e) => e.isTrashed);
|
||||||
|
if (trashUpdates.isEmpty) {
|
||||||
|
return Future.value();
|
||||||
|
}
|
||||||
|
final trashedAssets = <TrashedAsset>[];
|
||||||
|
for (final update in trashUpdates) {
|
||||||
|
final albums = delta.assetAlbums.cast<String, List<Object?>>();
|
||||||
|
for (final String id in albums[update.id]!.cast<String?>().nonNulls) {
|
||||||
|
trashedAssets.add(update.toTrashedAsset(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_logger.info("updateLocalTrashChanges trashedAssets: ${trashedAssets.map((e) => e.id)}");
|
||||||
|
await _trashedLocalAssetRepository.insertTrashDelta(trashedAssets);
|
||||||
|
// todo find for more suitable place
|
||||||
|
await applyRemoteRestoreToLocal();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> handleRemoteChanges(Iterable<String> checksums) async {
|
Future<void> handleRemoteChanges(Iterable<String> checksums) async {
|
||||||
if (checksums.isEmpty) {
|
if (checksums.isEmpty) {
|
||||||
return Future.value();
|
return Future.value();
|
||||||
|
|
@ -93,6 +111,10 @@ class TrashSyncService {
|
||||||
_logger.info("Restoring from trash, localId: ${asset.id}, remoteId: ${asset.checksum}");
|
_logger.info("Restoring from trash, localId: ${asset.id}, remoteId: ${asset.checksum}");
|
||||||
await _localFilesManager.restoreFromTrashById(asset.id, asset.type.index);
|
await _localFilesManager.restoreFromTrashById(asset.id, asset.type.index);
|
||||||
}
|
}
|
||||||
|
// todo 19/09/2025
|
||||||
|
// 1. keeping full mirror of local asset table struct + size into trashedLocalAssetEntity could help to restore assets here
|
||||||
|
// 2. now when hash calculating doing without taking into account size of files, size field may be redundant
|
||||||
|
|
||||||
// todo It`s necessary? could cause race with deletion in applyTrashSnapshot? 18/09/2025
|
// todo It`s necessary? could cause race with deletion in applyTrashSnapshot? 18/09/2025
|
||||||
await _trashedLocalAssetRepository.delete(remoteAssetsToRestore.map((e) => e.id));
|
await _trashedLocalAssetRepository.delete(remoteAssetsToRestore.map((e) => e.id));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -101,19 +123,21 @@ class TrashSyncService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on Iterable<PlatformAsset> {
|
extension on PlatformAsset {
|
||||||
List<TrashedAsset> toTrashedAssets(String albumId) {
|
TrashedAsset toTrashedAsset(String albumId) => TrashedAsset(
|
||||||
return map(
|
id: id,
|
||||||
(e) => TrashedAsset(
|
name: name,
|
||||||
id: e.id,
|
checksum: null,
|
||||||
name: e.name,
|
type: AssetType.values.elementAtOrNull(type) ?? AssetType.other,
|
||||||
checksum: null,
|
createdAt: tryFromSecondsSinceEpoch(createdAt) ?? DateTime.now(),
|
||||||
type: AssetType.values.elementAtOrNull(e.type) ?? AssetType.other,
|
updatedAt: tryFromSecondsSinceEpoch(updatedAt) ?? DateTime.now(),
|
||||||
createdAt: tryFromSecondsSinceEpoch(e.createdAt) ?? DateTime.now(),
|
size: size,
|
||||||
updatedAt: tryFromSecondsSinceEpoch(e.updatedAt) ?? DateTime.now(),
|
albumId: albumId,
|
||||||
size: e.size,
|
);
|
||||||
albumId: albumId,
|
}
|
||||||
),
|
|
||||||
).toList();
|
extension PlatformAssetsExtension on Iterable<PlatformAsset> {
|
||||||
|
Iterable<TrashedAsset> toTrashedAssets(String albumId) {
|
||||||
|
return map((e) => e.toTrashedAsset(albumId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,12 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
||||||
if (assets.isEmpty) {
|
if (assets.isEmpty) {
|
||||||
return Future.value();
|
return Future.value();
|
||||||
}
|
}
|
||||||
|
final now = DateTime.now();
|
||||||
return _db.batch((batch) async {
|
return _db.batch((batch) async {
|
||||||
for (final asset in assets) {
|
for (final asset in assets) {
|
||||||
batch.update(
|
batch.update(
|
||||||
_db.trashedLocalAssetEntity,
|
_db.trashedLocalAssetEntity,
|
||||||
TrashedLocalAssetEntityCompanion(checksum: Value(asset.checksum)),
|
TrashedLocalAssetEntityCompanion(checksum: Value(asset.checksum), updatedAt: Value(now)),
|
||||||
where: (e) => e.id.equals(asset.id),
|
where: (e) => e.id.equals(asset.id),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -95,6 +95,30 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> insertTrashDelta(Iterable<TrashedAsset> trashUpdates) async {
|
||||||
|
if (trashUpdates.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final companions = trashUpdates
|
||||||
|
.map(
|
||||||
|
(a) => TrashedLocalAssetEntityCompanion.insert(
|
||||||
|
id: a.id,
|
||||||
|
albumId: a.albumId,
|
||||||
|
name: a.name,
|
||||||
|
type: a.type,
|
||||||
|
checksum: a.checksum == null ? const Value.absent() : Value(a.checksum),
|
||||||
|
size: a.size == null ? const Value.absent() : Value(a.size),
|
||||||
|
createdAt: Value(a.createdAt),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final slice in companions.slices(200)) {
|
||||||
|
await _db.batch((b) {
|
||||||
|
b.insertAllOnConflictUpdate(_db.trashedLocalAssetEntity, slice);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Stream<int> watchCount() {
|
Stream<int> watchCount() {
|
||||||
final t = _db.trashedLocalAssetEntity;
|
final t = _db.trashedLocalAssetEntity;
|
||||||
return (_db.selectOnly(t)..addColumns([t.id.count()])).watchSingle().map((row) => row.read<int>(t.id.count()) ?? 0);
|
return (_db.selectOnly(t)..addColumns([t.id.count()])).watchSingle().map((row) => row.read<int>(t.id.count()) ?? 0);
|
||||||
|
|
|
||||||
7
mobile/lib/platform/native_sync_api.g.dart
generated
7
mobile/lib/platform/native_sync_api.g.dart
generated
|
|
@ -41,6 +41,7 @@ class PlatformAsset {
|
||||||
required this.durationInSeconds,
|
required this.durationInSeconds,
|
||||||
required this.orientation,
|
required this.orientation,
|
||||||
required this.isFavorite,
|
required this.isFavorite,
|
||||||
|
required this.isTrashed,
|
||||||
this.size,
|
this.size,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -64,6 +65,8 @@ class PlatformAsset {
|
||||||
|
|
||||||
bool isFavorite;
|
bool isFavorite;
|
||||||
|
|
||||||
|
bool isTrashed;
|
||||||
|
|
||||||
int? size;
|
int? size;
|
||||||
|
|
||||||
List<Object?> _toList() {
|
List<Object?> _toList() {
|
||||||
|
|
@ -78,6 +81,7 @@ class PlatformAsset {
|
||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
orientation,
|
orientation,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
|
isTrashed,
|
||||||
size,
|
size,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -99,7 +103,8 @@ class PlatformAsset {
|
||||||
durationInSeconds: result[7]! as int,
|
durationInSeconds: result[7]! as int,
|
||||||
orientation: result[8]! as int,
|
orientation: result[8]! as int,
|
||||||
isFavorite: result[9]! as bool,
|
isFavorite: result[9]! as bool,
|
||||||
size: result[10] as int?,
|
isTrashed: result[10]! as bool,
|
||||||
|
size: result[11] as int?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ class PlatformAsset {
|
||||||
final int durationInSeconds;
|
final int durationInSeconds;
|
||||||
final int orientation;
|
final int orientation;
|
||||||
final bool isFavorite;
|
final bool isFavorite;
|
||||||
|
final bool isTrashed;
|
||||||
final int? size;
|
final int? size;
|
||||||
|
|
||||||
const PlatformAsset({
|
const PlatformAsset({
|
||||||
|
|
@ -37,6 +38,7 @@ class PlatformAsset {
|
||||||
this.durationInSeconds = 0,
|
this.durationInSeconds = 0,
|
||||||
this.orientation = 0,
|
this.orientation = 0,
|
||||||
this.isFavorite = false,
|
this.isFavorite = false,
|
||||||
|
this.isTrashed = false,
|
||||||
this.size,
|
this.size,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue