#1427 increased precision of file modified date to milliseconds
This commit is contained in:
parent
9280b4a6a7
commit
5f26cfbbf3
25 changed files with 153 additions and 96 deletions
|
@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
### Changed
|
||||
|
||||
- increased precision of file modified date to milliseconds
|
||||
- upgraded Flutter to stable v3.29.0
|
||||
|
||||
### Fixed
|
||||
|
|
|
@ -8,6 +8,7 @@ import deckers.thibault.aves.channel.calls.fetchers.RegionFetcher
|
|||
import deckers.thibault.aves.channel.calls.fetchers.SvgRegionFetcher
|
||||
import deckers.thibault.aves.channel.calls.fetchers.ThumbnailFetcher
|
||||
import deckers.thibault.aves.channel.calls.fetchers.TiffRegionFetcher
|
||||
import deckers.thibault.aves.model.EntryFields
|
||||
import deckers.thibault.aves.utils.MimeTypes
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
@ -34,11 +35,11 @@ class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
|
|||
}
|
||||
|
||||
private suspend fun getThumbnail(call: MethodCall, result: MethodChannel.Result) {
|
||||
val uri = call.argument<String>("uri")
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
val dateModifiedSecs = call.argument<Number>("dateModifiedSecs")?.toLong()
|
||||
val rotationDegrees = call.argument<Int>("rotationDegrees")
|
||||
val isFlipped = call.argument<Boolean>("isFlipped")
|
||||
val uri = call.argument<String>(EntryFields.URI)
|
||||
val mimeType = call.argument<String>(EntryFields.MIME_TYPE)
|
||||
val dateModifiedMillis = call.argument<Number>(EntryFields.DATE_MODIFIED_MILLIS)?.toLong()
|
||||
val rotationDegrees = call.argument<Int>(EntryFields.ROTATION_DEGREES)
|
||||
val isFlipped = call.argument<Boolean>(EntryFields.IS_FLIPPED)
|
||||
val widthDip = call.argument<Number>("widthDip")?.toDouble()
|
||||
val heightDip = call.argument<Number>("heightDip")?.toDouble()
|
||||
val pageId = call.argument<Int>("pageId")
|
||||
|
@ -55,7 +56,7 @@ class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
|
|||
context = context,
|
||||
uri = uri,
|
||||
mimeType = mimeType,
|
||||
dateModifiedSecs = dateModifiedSecs ?: (Date().time / 1000),
|
||||
dateModifiedMillis = dateModifiedMillis ?: (Date().time),
|
||||
rotationDegrees = rotationDegrees,
|
||||
isFlipped = isFlipped,
|
||||
width = (widthDip * density).roundToInt(),
|
||||
|
|
|
@ -30,7 +30,7 @@ class ThumbnailFetcher internal constructor(
|
|||
private val context: Context,
|
||||
uri: String,
|
||||
private val mimeType: String,
|
||||
private val dateModifiedSecs: Long,
|
||||
private val dateModifiedMillis: Long,
|
||||
private val rotationDegrees: Int,
|
||||
private val isFlipped: Boolean,
|
||||
width: Int?,
|
||||
|
@ -119,7 +119,7 @@ class ThumbnailFetcher internal constructor(
|
|||
// add signature to ignore cache for images which got modified but kept the same URI
|
||||
var options = RequestOptions()
|
||||
.format(if (quality == 100) DecodeFormat.PREFER_ARGB_8888 else DecodeFormat.PREFER_RGB_565)
|
||||
.signature(ObjectKey("$dateModifiedSecs-$rotationDegrees-$isFlipped-$width-$pageId"))
|
||||
.signature(ObjectKey("$dateModifiedMillis-$rotationDegrees-$isFlipped-$width-$pageId"))
|
||||
.override(width, height)
|
||||
if (isVideo(mimeType)) {
|
||||
options = options.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
|
|
|
@ -19,12 +19,13 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
|
|||
private lateinit var eventSink: EventSink
|
||||
private lateinit var handler: Handler
|
||||
|
||||
private var knownEntries: Map<Long?, Int?>? = null
|
||||
// knownEntries: map of contentId -> dateModifiedMillis
|
||||
private var knownEntries: Map<Long?, Long?>? = null
|
||||
private var directory: String? = null
|
||||
|
||||
init {
|
||||
if (arguments is Map<*, *>) {
|
||||
knownEntries = (arguments["knownEntries"] as? Map<*, *>?)?.map { (it.key as Number?)?.toLong() to it.value as Int? }?.toMap()
|
||||
knownEntries = (arguments["knownEntries"] as? Map<*, *>?)?.map { (it.key as Number?)?.toLong() to (it.value as Number?)?.toLong() }?.toMap()
|
||||
directory = arguments["directory"] as String?
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ object EntryFields {
|
|||
const val IS_FLIPPED = "isFlipped" // boolean
|
||||
|
||||
const val DATE_ADDED_SECS = "dateAddedSecs" // long
|
||||
const val DATE_MODIFIED_SECS = "dateModifiedSecs" // long
|
||||
const val DATE_MODIFIED_MILLIS = "dateModifiedMillis" // long
|
||||
const val SOURCE_DATE_TAKEN_MILLIS = "sourceDateTakenMillis" // long
|
||||
const val DURATION_MILLIS = "durationMillis" // long
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ class SourceEntry {
|
|||
private var sourceRotationDegrees: Int? = null
|
||||
private var sizeBytes: Long? = null
|
||||
private var dateAddedSecs: Long? = null
|
||||
private var dateModifiedSecs: Long? = null
|
||||
private var dateModifiedMillis: Long? = null
|
||||
private var sourceDateTakenMillis: Long? = null
|
||||
private var durationMillis: Long? = null
|
||||
|
||||
|
@ -65,16 +65,16 @@ class SourceEntry {
|
|||
sizeBytes = toLong(map[EntryFields.SIZE_BYTES])
|
||||
title = map[EntryFields.TITLE] as String?
|
||||
dateAddedSecs = toLong(map[EntryFields.DATE_ADDED_SECS])
|
||||
dateModifiedSecs = toLong(map[EntryFields.DATE_MODIFIED_SECS])
|
||||
dateModifiedMillis = toLong(map[EntryFields.DATE_MODIFIED_MILLIS])
|
||||
sourceDateTakenMillis = toLong(map[EntryFields.SOURCE_DATE_TAKEN_MILLIS])
|
||||
durationMillis = toLong(map[EntryFields.DURATION_MILLIS])
|
||||
}
|
||||
|
||||
fun initFromFile(path: String, title: String, sizeBytes: Long, dateModifiedSecs: Long) {
|
||||
fun initFromFile(path: String, title: String, sizeBytes: Long, dateModifiedMillis: Long) {
|
||||
this.path = path
|
||||
this.title = title
|
||||
this.sizeBytes = sizeBytes
|
||||
this.dateModifiedSecs = dateModifiedSecs
|
||||
this.dateModifiedMillis = dateModifiedMillis
|
||||
}
|
||||
|
||||
fun toMap(): FieldMap {
|
||||
|
@ -89,7 +89,7 @@ class SourceEntry {
|
|||
EntryFields.SIZE_BYTES to sizeBytes,
|
||||
EntryFields.TITLE to title,
|
||||
EntryFields.DATE_ADDED_SECS to dateAddedSecs,
|
||||
EntryFields.DATE_MODIFIED_SECS to dateModifiedSecs,
|
||||
EntryFields.DATE_MODIFIED_MILLIS to dateModifiedMillis,
|
||||
EntryFields.SOURCE_DATE_TAKEN_MILLIS to sourceDateTakenMillis,
|
||||
EntryFields.DURATION_MILLIS to durationMillis,
|
||||
// only for map export
|
||||
|
|
|
@ -46,7 +46,7 @@ internal class FileImageProvider : ImageProvider() {
|
|||
path = path,
|
||||
title = file.name,
|
||||
sizeBytes = file.length(),
|
||||
dateModifiedSecs = file.lastModified() / 1000,
|
||||
dateModifiedMillis = file.lastModified(),
|
||||
)
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
|
@ -91,7 +91,7 @@ internal class FileImageProvider : ImageProvider() {
|
|||
return hashMapOf(
|
||||
EntryFields.URI to Uri.fromFile(newFile).toString(),
|
||||
EntryFields.PATH to newFile.path,
|
||||
EntryFields.DATE_MODIFIED_SECS to newFile.lastModified() / 1000,
|
||||
EntryFields.DATE_MODIFIED_MILLIS to newFile.lastModified(),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,7 @@ internal class FileImageProvider : ImageProvider() {
|
|||
try {
|
||||
val file = File(path)
|
||||
if (file.exists()) {
|
||||
newFields[EntryFields.DATE_MODIFIED_SECS] = file.lastModified() / 1000
|
||||
newFields[EntryFields.DATE_MODIFIED_MILLIS] = file.lastModified()
|
||||
newFields[EntryFields.SIZE_BYTES] = file.length()
|
||||
}
|
||||
callback.onSuccess(newFields)
|
||||
|
|
|
@ -51,14 +51,14 @@ import kotlin.coroutines.suspendCoroutine
|
|||
class MediaStoreImageProvider : ImageProvider() {
|
||||
fun fetchAll(
|
||||
context: Context,
|
||||
knownEntries: Map<Long?, Int?>,
|
||||
knownEntries: Map<Long?, Long?>,
|
||||
directory: String?,
|
||||
handleNewEntry: NewEntryHandler,
|
||||
) {
|
||||
Log.d(LOG_TAG, "fetching all media store items for ${knownEntries.size} known entries, directory=$directory")
|
||||
val isModified = fun(contentId: Long, dateModifiedSecs: Int): Boolean {
|
||||
val isModified = fun(contentId: Long, dateModifiedMillis: Long): Boolean {
|
||||
val knownDate = knownEntries[contentId]
|
||||
return knownDate == null || knownDate < dateModifiedSecs
|
||||
return knownDate == null || knownDate < dateModifiedMillis
|
||||
}
|
||||
val handleNew: NewEntryHandler
|
||||
var selection: String? = null
|
||||
|
@ -96,7 +96,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
var found = false
|
||||
val fetched = arrayListOf<FieldMap>()
|
||||
val id = uri.tryParseId()
|
||||
val alwaysValid: NewEntryChecker = fun(_: Long, _: Int): Boolean = true
|
||||
val alwaysValid: NewEntryChecker = fun(_: Long, _: Long): Boolean = true
|
||||
val onSuccess: NewEntryHandler = fun(entry: FieldMap) { fetched.add(entry) }
|
||||
if (id != null) {
|
||||
if (sourceMimeType == null || isImage(sourceMimeType)) {
|
||||
|
@ -227,8 +227,8 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)
|
||||
val widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH)
|
||||
val heightColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT)
|
||||
val dateAddedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED)
|
||||
val dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)
|
||||
val dateAddedSecsColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED)
|
||||
val dateModifiedSecsColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)
|
||||
val dateTakenColumn = cursor.getColumnIndex(MediaColumns.DATE_TAKEN)
|
||||
|
||||
// image & video for API >=29, only for images for API <29
|
||||
|
@ -240,8 +240,8 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idColumn)
|
||||
val dateModifiedSecs = cursor.getInt(dateModifiedColumn)
|
||||
if (isValidEntry(id, dateModifiedSecs)) {
|
||||
val dateModifiedMillis = cursor.getInt(dateModifiedSecsColumn) * 1000L
|
||||
if (isValidEntry(id, dateModifiedMillis)) {
|
||||
// for multiple items, `contentUri` is the root without ID,
|
||||
// but for single items, `contentUri` already contains the ID
|
||||
val itemUri = if (contentUriContainsId) contentUri else ContentUris.withAppendedId(contentUri, id)
|
||||
|
@ -255,17 +255,18 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
if (mimeType == null) {
|
||||
Log.w(LOG_TAG, "failed to make entry from uri=$itemUri because of null MIME type")
|
||||
} else {
|
||||
var entryMap: FieldMap = hashMapOf(
|
||||
val path = cursor.getString(pathColumn)
|
||||
var entryFields: FieldMap = hashMapOf(
|
||||
EntryFields.ORIGIN to SourceEntry.ORIGIN_MEDIA_STORE_CONTENT,
|
||||
EntryFields.URI to itemUri.toString(),
|
||||
EntryFields.PATH to cursor.getString(pathColumn),
|
||||
EntryFields.PATH to path,
|
||||
EntryFields.SOURCE_MIME_TYPE to mimeType,
|
||||
EntryFields.WIDTH to width,
|
||||
EntryFields.HEIGHT to height,
|
||||
EntryFields.SOURCE_ROTATION_DEGREES to if (orientationColumn != -1) cursor.getInt(orientationColumn) else 0,
|
||||
EntryFields.SIZE_BYTES to cursor.getLong(sizeColumn),
|
||||
EntryFields.DATE_ADDED_SECS to cursor.getInt(dateAddedColumn),
|
||||
EntryFields.DATE_MODIFIED_SECS to dateModifiedSecs,
|
||||
EntryFields.DATE_ADDED_SECS to cursor.getInt(dateAddedSecsColumn),
|
||||
EntryFields.DATE_MODIFIED_MILLIS to dateModifiedMillis,
|
||||
EntryFields.SOURCE_DATE_TAKEN_MILLIS to if (dateTakenColumn != -1) cursor.getLong(dateTakenColumn) else null,
|
||||
EntryFields.DURATION_MILLIS to durationMillis,
|
||||
// only for map export
|
||||
|
@ -285,8 +286,8 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
if (outWidth > 0 && outHeight > 0) {
|
||||
width = outWidth
|
||||
height = outHeight
|
||||
entryMap[EntryFields.WIDTH] = width
|
||||
entryMap[EntryFields.HEIGHT] = height
|
||||
entryFields[EntryFields.WIDTH] = width
|
||||
entryFields[EntryFields.HEIGHT] = height
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
|
@ -302,11 +303,13 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
// missing some attributes such as width, height, orientation.
|
||||
// Also, the reported size of raw images is inconsistent across devices
|
||||
// and Android versions (sometimes the raw size, sometimes the decoded size).
|
||||
val entry = SourceEntry(entryMap).fillPreCatalogMetadata(context)
|
||||
entryMap = entry.toMap()
|
||||
val entry = SourceEntry(entryFields).fillPreCatalogMetadata(context)
|
||||
entryFields = entry.toMap()
|
||||
}
|
||||
|
||||
handleNewEntry(entryMap)
|
||||
getFileModifiedDateMillis(path)?.let { entryFields[EntryFields.DATE_MODIFIED_MILLIS] = it }
|
||||
|
||||
handleNewEntry(entryFields)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
@ -823,18 +826,32 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
try {
|
||||
val cursor = context.contentResolver.query(uri, projection, null, null, null)
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) }
|
||||
cursor.getColumnIndex(MediaStore.MediaColumns.SIZE).let { if (it != -1) newFields["sizeBytes"] = cursor.getLong(it) }
|
||||
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields[EntryFields.DATE_MODIFIED_MILLIS] = cursor.getInt(it) * 1000 }
|
||||
cursor.getColumnIndex(MediaStore.MediaColumns.SIZE).let { if (it != -1) newFields[EntryFields.SIZE_BYTES] = cursor.getLong(it) }
|
||||
cursor.close()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
callback.onFailure(e)
|
||||
return@scanFile
|
||||
}
|
||||
getFileModifiedDateMillis(path)?.let { newFields[EntryFields.DATE_MODIFIED_MILLIS] = it }
|
||||
callback.onSuccess(newFields)
|
||||
}
|
||||
}
|
||||
|
||||
// try to fetch the modified date from the file,
|
||||
// as it is more precise than the one from the Media Store
|
||||
private fun getFileModifiedDateMillis(path: String?): Long? {
|
||||
if (path != null) {
|
||||
try {
|
||||
return File(path).lastModified()
|
||||
} catch (securityException: SecurityException) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun scanObsoletePath(context: Context, uri: Uri, path: String, mimeType: String) {
|
||||
val file = File(path)
|
||||
val delayMillis = 500L
|
||||
|
@ -918,8 +935,9 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
EntryFields.PATH to path,
|
||||
)
|
||||
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_ADDED).let { if (it != -1) newFields[EntryFields.DATE_ADDED_SECS] = cursor.getInt(it) }
|
||||
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields[EntryFields.DATE_MODIFIED_SECS] = cursor.getInt(it) }
|
||||
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields[EntryFields.DATE_MODIFIED_MILLIS] = cursor.getInt(it) * 1000 }
|
||||
cursor.close()
|
||||
getFileModifiedDateMillis(path)?.let { newFields[EntryFields.DATE_MODIFIED_MILLIS] = it }
|
||||
return newFields
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
@ -1030,4 +1048,4 @@ object MediaColumns {
|
|||
|
||||
typealias NewEntryHandler = (entry: FieldMap) -> Unit
|
||||
|
||||
private typealias NewEntryChecker = (contentId: Long, dateModifiedSecs: Int) -> Boolean
|
||||
private typealias NewEntryChecker = (contentId: Long, dateModifiedMillis: Long) -> Boolean
|
|
@ -42,7 +42,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
|||
pageId: pageId,
|
||||
rotationDegrees: key.rotationDegrees,
|
||||
isFlipped: key.isFlipped,
|
||||
dateModifiedSecs: key.dateModifiedSecs,
|
||||
dateModifiedMillis: key.dateModifiedMillis,
|
||||
extent: key.extent,
|
||||
taskKey: key,
|
||||
);
|
||||
|
@ -75,11 +75,11 @@ class ThumbnailProviderKey extends Equatable {
|
|||
final int? pageId;
|
||||
final int rotationDegrees;
|
||||
final bool isFlipped;
|
||||
final int dateModifiedSecs;
|
||||
final int dateModifiedMillis;
|
||||
final double extent;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [uri, pageId, dateModifiedSecs, extent];
|
||||
List<Object?> get props => [uri, pageId, dateModifiedMillis, extent];
|
||||
|
||||
const ThumbnailProviderKey({
|
||||
required this.uri,
|
||||
|
@ -87,10 +87,10 @@ class ThumbnailProviderKey extends Equatable {
|
|||
required this.pageId,
|
||||
required this.rotationDegrees,
|
||||
required this.isFlipped,
|
||||
required this.dateModifiedSecs,
|
||||
required this.dateModifiedMillis,
|
||||
this.extent = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, pageId=$pageId, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, dateModifiedSecs=$dateModifiedSecs, extent=$extent}';
|
||||
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, pageId=$pageId, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, dateModifiedMillis=$dateModifiedMillis, extent=$extent}';
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
|
|||
', sizeBytes INTEGER'
|
||||
', title TEXT'
|
||||
', dateAddedSecs INTEGER DEFAULT (strftime(\'%s\',\'now\'))'
|
||||
', dateModifiedSecs INTEGER'
|
||||
', dateModifiedMillis INTEGER'
|
||||
', sourceDateTakenMillis INTEGER'
|
||||
', durationMillis INTEGER'
|
||||
', trashed INTEGER DEFAULT 0'
|
||||
|
@ -117,7 +117,7 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
|
|||
')');
|
||||
},
|
||||
onUpgrade: LocalMediaDbUpgrader.upgradeDb,
|
||||
version: 13,
|
||||
version: 14,
|
||||
);
|
||||
|
||||
final maxIdRows = await _db.rawQuery('SELECT MAX(id) AS maxId FROM $entryTable');
|
||||
|
|
|
@ -47,6 +47,8 @@ class LocalMediaDbUpgrader {
|
|||
await _upgradeFrom11(db);
|
||||
case 12:
|
||||
await _upgradeFrom12(db);
|
||||
case 13:
|
||||
await _upgradeFrom13(db);
|
||||
}
|
||||
oldVersion++;
|
||||
}
|
||||
|
@ -444,4 +446,37 @@ class LocalMediaDbUpgrader {
|
|||
await db.execute('ALTER TABLE $newCoverTable RENAME TO $coverTable;');
|
||||
});
|
||||
}
|
||||
|
||||
static Future<void> _upgradeFrom13(Database db) async {
|
||||
debugPrint('upgrading DB from v13');
|
||||
|
||||
// rename column 'dateModifiedSecs' to 'dateModifiedMillis'
|
||||
await db.transaction((txn) async {
|
||||
const newEntryTable = '${entryTable}TEMP';
|
||||
await db.execute('CREATE TABLE $newEntryTable('
|
||||
'id INTEGER PRIMARY KEY'
|
||||
', contentId INTEGER'
|
||||
', uri TEXT'
|
||||
', path TEXT'
|
||||
', sourceMimeType TEXT'
|
||||
', width INTEGER'
|
||||
', height INTEGER'
|
||||
', sourceRotationDegrees INTEGER'
|
||||
', sizeBytes INTEGER'
|
||||
', title TEXT'
|
||||
', dateAddedSecs INTEGER DEFAULT (strftime(\'%s\',\'now\'))'
|
||||
', dateModifiedMillis INTEGER'
|
||||
', sourceDateTakenMillis INTEGER'
|
||||
', durationMillis INTEGER'
|
||||
', trashed INTEGER DEFAULT 0'
|
||||
', origin INTEGER DEFAULT 0'
|
||||
')');
|
||||
await db.rawInsert('INSERT INTO $newEntryTable(contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateAddedSecs,dateModifiedMillis,sourceDateTakenMillis,durationMillis,trashed,origin)'
|
||||
' SELECT contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateAddedSecs,dateModifiedSecs*1000,sourceDateTakenMillis,durationMillis,trashed,origin'
|
||||
' FROM $entryTable;');
|
||||
await db.execute('DROP TABLE $entryTable;');
|
||||
await db.execute('ALTER TABLE $newEntryTable RENAME TO $entryTable;');
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,11 +19,11 @@ class EntryCache {
|
|||
static Future<void> evict(
|
||||
String uri,
|
||||
String mimeType,
|
||||
int? dateModifiedSecs,
|
||||
int? dateModifiedMillis,
|
||||
int rotationDegrees,
|
||||
bool isFlipped,
|
||||
) async {
|
||||
debugPrint('Evict cached images for uri=$uri, mimeType=$mimeType, dateModifiedSecs=$dateModifiedSecs, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped');
|
||||
debugPrint('Evict cached images for uri=$uri, mimeType=$mimeType, dateModifiedMillis=$dateModifiedMillis, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped');
|
||||
|
||||
// TODO TLAD provide pageId parameter for multi page items, if someday image editing features are added for them
|
||||
int? pageId;
|
||||
|
@ -42,7 +42,7 @@ class EntryCache {
|
|||
uri: uri,
|
||||
mimeType: mimeType,
|
||||
pageId: pageId,
|
||||
dateModifiedSecs: dateModifiedSecs ?? 0,
|
||||
dateModifiedMillis: dateModifiedMillis ?? 0,
|
||||
rotationDegrees: rotationDegrees,
|
||||
isFlipped: isFlipped,
|
||||
)).evict();
|
||||
|
@ -53,7 +53,7 @@ class EntryCache {
|
|||
uri: uri,
|
||||
mimeType: mimeType,
|
||||
pageId: pageId,
|
||||
dateModifiedSecs: dateModifiedSecs ?? 0,
|
||||
dateModifiedMillis: dateModifiedMillis ?? 0,
|
||||
rotationDegrees: rotationDegrees,
|
||||
isFlipped: isFlipped,
|
||||
extent: extent,
|
||||
|
|
|
@ -36,7 +36,7 @@ class AvesEntry with AvesEntryBase {
|
|||
int? contentId;
|
||||
final String sourceMimeType;
|
||||
int width, height, sourceRotationDegrees;
|
||||
int? dateAddedSecs, _dateModifiedSecs, sourceDateTakenMillis, _durationMillis;
|
||||
int? dateAddedSecs, _dateModifiedMillis, sourceDateTakenMillis, _durationMillis;
|
||||
bool trashed;
|
||||
int origin;
|
||||
|
||||
|
@ -66,7 +66,7 @@ class AvesEntry with AvesEntryBase {
|
|||
required this.sizeBytes,
|
||||
required String? sourceTitle,
|
||||
required this.dateAddedSecs,
|
||||
required int? dateModifiedSecs,
|
||||
required int? dateModifiedMillis,
|
||||
required this.sourceDateTakenMillis,
|
||||
required int? durationMillis,
|
||||
required this.trashed,
|
||||
|
@ -82,7 +82,7 @@ class AvesEntry with AvesEntryBase {
|
|||
}
|
||||
this.path = path;
|
||||
this.sourceTitle = sourceTitle;
|
||||
this.dateModifiedSecs = dateModifiedSecs;
|
||||
this.dateModifiedMillis = dateModifiedMillis;
|
||||
this.durationMillis = durationMillis;
|
||||
}
|
||||
|
||||
|
@ -93,7 +93,7 @@ class AvesEntry with AvesEntryBase {
|
|||
int? contentId,
|
||||
String? title,
|
||||
int? dateAddedSecs,
|
||||
int? dateModifiedSecs,
|
||||
int? dateModifiedMillis,
|
||||
int? origin,
|
||||
List<AvesEntry>? stackedEntries,
|
||||
}) {
|
||||
|
@ -111,7 +111,7 @@ class AvesEntry with AvesEntryBase {
|
|||
sizeBytes: sizeBytes,
|
||||
sourceTitle: title ?? sourceTitle,
|
||||
dateAddedSecs: dateAddedSecs ?? this.dateAddedSecs,
|
||||
dateModifiedSecs: dateModifiedSecs ?? this.dateModifiedSecs,
|
||||
dateModifiedMillis: dateModifiedMillis ?? this.dateModifiedMillis,
|
||||
sourceDateTakenMillis: sourceDateTakenMillis,
|
||||
durationMillis: durationMillis,
|
||||
trashed: trashed,
|
||||
|
@ -140,7 +140,7 @@ class AvesEntry with AvesEntryBase {
|
|||
sizeBytes: map[EntryFields.sizeBytes] as int?,
|
||||
sourceTitle: map[EntryFields.title] as String?,
|
||||
dateAddedSecs: map[EntryFields.dateAddedSecs] as int?,
|
||||
dateModifiedSecs: map[EntryFields.dateModifiedSecs] as int?,
|
||||
dateModifiedMillis: map[EntryFields.dateModifiedMillis] as int?,
|
||||
sourceDateTakenMillis: map[EntryFields.sourceDateTakenMillis] as int?,
|
||||
durationMillis: map[EntryFields.durationMillis] as int?,
|
||||
trashed: (map[EntryFields.trashed] as int? ?? 0) != 0,
|
||||
|
@ -162,7 +162,7 @@ class AvesEntry with AvesEntryBase {
|
|||
EntryFields.sizeBytes: sizeBytes,
|
||||
EntryFields.title: sourceTitle,
|
||||
EntryFields.dateAddedSecs: dateAddedSecs,
|
||||
EntryFields.dateModifiedSecs: dateModifiedSecs,
|
||||
EntryFields.dateModifiedMillis: dateModifiedMillis,
|
||||
EntryFields.sourceDateTakenMillis: sourceDateTakenMillis,
|
||||
EntryFields.durationMillis: durationMillis,
|
||||
EntryFields.trashed: trashed ? 1 : 0,
|
||||
|
@ -180,7 +180,7 @@ class AvesEntry with AvesEntryBase {
|
|||
EntryFields.height: height,
|
||||
EntryFields.rotationDegrees: rotationDegrees,
|
||||
EntryFields.isFlipped: isFlipped,
|
||||
EntryFields.dateModifiedSecs: dateModifiedSecs,
|
||||
EntryFields.dateModifiedMillis: dateModifiedMillis,
|
||||
EntryFields.sizeBytes: sizeBytes,
|
||||
EntryFields.trashed: trashed,
|
||||
EntryFields.trashPath: trashDetails?.path,
|
||||
|
@ -241,7 +241,7 @@ class AvesEntry with AvesEntryBase {
|
|||
DateTime? _bestDate;
|
||||
|
||||
DateTime? get bestDate {
|
||||
_bestDate ??= dateTimeFromMillis(_catalogDateMillis) ?? dateTimeFromMillis(sourceDateTakenMillis) ?? dateTimeFromMillis((dateModifiedSecs ?? 0) * 1000);
|
||||
_bestDate ??= dateTimeFromMillis(_catalogDateMillis) ?? dateTimeFromMillis(sourceDateTakenMillis) ?? dateTimeFromMillis(dateModifiedMillis ?? 0);
|
||||
return _bestDate;
|
||||
}
|
||||
|
||||
|
@ -292,10 +292,10 @@ class AvesEntry with AvesEntryBase {
|
|||
_bestTitle = null;
|
||||
}
|
||||
|
||||
int? get dateModifiedSecs => _dateModifiedSecs;
|
||||
int? get dateModifiedMillis => _dateModifiedMillis;
|
||||
|
||||
set dateModifiedSecs(int? dateModifiedSecs) {
|
||||
_dateModifiedSecs = dateModifiedSecs;
|
||||
set dateModifiedMillis(int? dateModifiedMillis) {
|
||||
_dateModifiedMillis = dateModifiedMillis;
|
||||
_bestDate = null;
|
||||
}
|
||||
|
||||
|
@ -362,7 +362,7 @@ class AvesEntry with AvesEntryBase {
|
|||
|
||||
set catalogMetadata(CatalogMetadata? newMetadata) {
|
||||
final oldMimeType = mimeType;
|
||||
final oldDateModifiedSecs = dateModifiedSecs;
|
||||
final oldDateModifiedMillis = dateModifiedMillis;
|
||||
final oldRotationDegrees = rotationDegrees;
|
||||
final oldIsFlipped = isFlipped;
|
||||
|
||||
|
@ -372,7 +372,7 @@ class AvesEntry with AvesEntryBase {
|
|||
_tags = null;
|
||||
metadataChangeNotifier.notify();
|
||||
|
||||
_onVisualFieldChanged(oldMimeType, oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped);
|
||||
_onVisualFieldChanged(oldMimeType, oldDateModifiedMillis, oldRotationDegrees, oldIsFlipped);
|
||||
}
|
||||
|
||||
void clearMetadata() {
|
||||
|
@ -399,7 +399,7 @@ class AvesEntry with AvesEntryBase {
|
|||
|
||||
Future<void> applyNewFields(Map newFields, {required bool persist}) async {
|
||||
final oldMimeType = mimeType;
|
||||
final oldDateModifiedSecs = this.dateModifiedSecs;
|
||||
final oldDateModifiedMillis = this.dateModifiedMillis;
|
||||
final oldRotationDegrees = this.rotationDegrees;
|
||||
final oldIsFlipped = this.isFlipped;
|
||||
|
||||
|
@ -426,8 +426,8 @@ class AvesEntry with AvesEntryBase {
|
|||
|
||||
final sizeBytes = newFields[EntryFields.sizeBytes];
|
||||
if (sizeBytes is int) this.sizeBytes = sizeBytes;
|
||||
final dateModifiedSecs = newFields[EntryFields.dateModifiedSecs];
|
||||
if (dateModifiedSecs is int) this.dateModifiedSecs = dateModifiedSecs;
|
||||
final dateModifiedMillis = newFields[EntryFields.dateModifiedMillis];
|
||||
if (dateModifiedMillis is int) this.dateModifiedMillis = dateModifiedMillis;
|
||||
final rotationDegrees = newFields[EntryFields.rotationDegrees];
|
||||
if (rotationDegrees is int) this.rotationDegrees = rotationDegrees;
|
||||
final isFlipped = newFields[EntryFields.isFlipped];
|
||||
|
@ -438,7 +438,7 @@ class AvesEntry with AvesEntryBase {
|
|||
if (catalogMetadata != null) await localMediaDb.saveCatalogMetadata({catalogMetadata!});
|
||||
}
|
||||
|
||||
await _onVisualFieldChanged(oldMimeType, oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped);
|
||||
await _onVisualFieldChanged(oldMimeType, oldDateModifiedMillis, oldRotationDegrees, oldIsFlipped);
|
||||
metadataChangeNotifier.notify();
|
||||
}
|
||||
|
||||
|
@ -479,12 +479,12 @@ class AvesEntry with AvesEntryBase {
|
|||
// when the MIME type or the image itself changed (e.g. after rotation)
|
||||
Future<void> _onVisualFieldChanged(
|
||||
String oldMimeType,
|
||||
int? oldDateModifiedSecs,
|
||||
int? oldDateModifiedMillis,
|
||||
int oldRotationDegrees,
|
||||
bool oldIsFlipped,
|
||||
) async {
|
||||
if ((!MimeTypes.refersToSameType(oldMimeType, mimeType) && !MimeTypes.isVideo(oldMimeType)) || oldDateModifiedSecs != dateModifiedSecs || oldRotationDegrees != rotationDegrees || oldIsFlipped != isFlipped) {
|
||||
await EntryCache.evict(uri, oldMimeType, oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped);
|
||||
if ((!MimeTypes.refersToSameType(oldMimeType, mimeType) && !MimeTypes.isVideo(oldMimeType)) || oldDateModifiedMillis != dateModifiedMillis || oldRotationDegrees != rotationDegrees || oldIsFlipped != isFlipped) {
|
||||
await EntryCache.evict(uri, oldMimeType, oldDateModifiedMillis, oldRotationDegrees, oldIsFlipped);
|
||||
visualChangeNotifier.notify();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ extension ExtraAvesEntryImages on AvesEntry {
|
|||
pageId: pageId,
|
||||
rotationDegrees: rotationDegrees,
|
||||
isFlipped: isFlipped,
|
||||
dateModifiedSecs: dateModifiedSecs ?? -1,
|
||||
dateModifiedMillis: dateModifiedMillis ?? -1,
|
||||
extent: requestExtent,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ class EntryFields {
|
|||
static const isFlipped = 'isFlipped'; // boolean
|
||||
|
||||
static const dateAddedSecs = 'dateAddedSecs'; // long
|
||||
static const dateModifiedSecs = 'dateModifiedSecs'; // long
|
||||
static const dateModifiedMillis = 'dateModifiedMillis'; // long
|
||||
static const sourceDateTakenMillis = 'sourceDateTakenMillis'; // long
|
||||
static const durationMillis = 'durationMillis'; // long
|
||||
|
||||
|
|
|
@ -120,7 +120,7 @@ class MultiPageInfo {
|
|||
sizeBytes: mainEntry.sizeBytes,
|
||||
sourceTitle: mainEntry.sourceTitle,
|
||||
dateAddedSecs: mainEntry.dateAddedSecs,
|
||||
dateModifiedSecs: mainEntry.dateModifiedSecs,
|
||||
dateModifiedMillis: mainEntry.dateModifiedMillis,
|
||||
sourceDateTakenMillis: mainEntry.sourceDateTakenMillis,
|
||||
durationMillis: pageInfo.durationMillis ?? mainEntry.durationMillis,
|
||||
trashed: trashed,
|
||||
|
|
|
@ -245,10 +245,10 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
|||
switch (key) {
|
||||
case EntryFields.contentId:
|
||||
entry.contentId = newValue as int?;
|
||||
case EntryFields.dateModifiedSecs:
|
||||
// `dateModifiedSecs` changes when moving entries to another directory,
|
||||
case EntryFields.dateModifiedMillis:
|
||||
// `dateModifiedMillis` changes when moving entries to another directory,
|
||||
// but it does not change when renaming the containing directory
|
||||
entry.dateModifiedSecs = newValue as int?;
|
||||
entry.dateModifiedMillis = newValue as int?;
|
||||
case EntryFields.path:
|
||||
entry.path = newValue as String?;
|
||||
case EntryFields.title:
|
||||
|
@ -369,7 +369,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
|||
// title can change when moved files are automatically renamed to avoid conflict
|
||||
title: newFields[EntryFields.title] as String?,
|
||||
dateAddedSecs: newFields[EntryFields.dateAddedSecs] as int?,
|
||||
dateModifiedSecs: newFields[EntryFields.dateModifiedSecs] as int?,
|
||||
dateModifiedMillis: newFields[EntryFields.dateModifiedMillis] as int?,
|
||||
origin: newFields[EntryFields.origin] as int?,
|
||||
));
|
||||
} else {
|
||||
|
|
|
@ -101,7 +101,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
final knownLiveEntries = knownEntries.where((entry) => !entry.trashed).toSet();
|
||||
|
||||
debugPrint('$runtimeType load ${stopwatch.elapsed} check obsolete entries');
|
||||
final knownDateByContentId = Map.fromEntries(knownLiveEntries.map((entry) => MapEntry(entry.contentId, entry.dateModifiedSecs)));
|
||||
final knownDateByContentId = Map.fromEntries(knownLiveEntries.map((entry) => MapEntry(entry.contentId, entry.dateModifiedMillis)));
|
||||
final knownContentIds = knownDateByContentId.keys.toList();
|
||||
final removedContentIds = (await mediaStoreService.checkObsoleteContentIds(knownContentIds)).toSet();
|
||||
if (topEntries.isNotEmpty) {
|
||||
|
@ -290,7 +290,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
if (sourceEntry != null) {
|
||||
final existingEntry = allEntries.firstWhereOrNull((entry) => entry.contentId == contentId);
|
||||
// compare paths because some apps move files without updating their `last modified date`
|
||||
if (existingEntry == null || (sourceEntry.dateModifiedSecs ?? 0) > (existingEntry.dateModifiedSecs ?? 0) || sourceEntry.path != existingEntry.path) {
|
||||
if (existingEntry == null || (sourceEntry.dateModifiedMillis ?? 0) > (existingEntry.dateModifiedMillis ?? 0) || sourceEntry.path != existingEntry.path) {
|
||||
final newPath = sourceEntry.path;
|
||||
final volume = newPath != null ? androidFileUtils.getStorageVolume(newPath) : null;
|
||||
if (volume != null) {
|
||||
|
|
|
@ -225,7 +225,7 @@ class PlatformAppService implements AppService {
|
|||
pageId: coverEntry.pageId,
|
||||
rotationDegrees: coverEntry.rotationDegrees,
|
||||
isFlipped: coverEntry.isFlipped,
|
||||
dateModifiedSecs: coverEntry.dateModifiedSecs,
|
||||
dateModifiedMillis: coverEntry.dateModifiedMillis,
|
||||
extent: size,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ abstract class MediaFetchService {
|
|||
required int rotationDegrees,
|
||||
required int? pageId,
|
||||
required bool isFlipped,
|
||||
required int? dateModifiedSecs,
|
||||
required int? dateModifiedMillis,
|
||||
required double extent,
|
||||
Object? taskKey,
|
||||
int? priority,
|
||||
|
@ -211,7 +211,7 @@ class PlatformMediaFetchService implements MediaFetchService {
|
|||
required int rotationDegrees,
|
||||
required int? pageId,
|
||||
required bool isFlipped,
|
||||
required int? dateModifiedSecs,
|
||||
required int? dateModifiedMillis,
|
||||
required double extent,
|
||||
Object? taskKey,
|
||||
int? priority,
|
||||
|
@ -222,7 +222,7 @@ class PlatformMediaFetchService implements MediaFetchService {
|
|||
final result = await _platformBytes.invokeMethod('getThumbnail', <String, dynamic>{
|
||||
'uri': uri,
|
||||
'mimeType': mimeType,
|
||||
'dateModifiedSecs': dateModifiedSecs,
|
||||
'dateModifiedMillis': dateModifiedMillis,
|
||||
'rotationDegrees': rotationDegrees,
|
||||
'isFlipped': isFlipped,
|
||||
'widthDip': extent,
|
||||
|
|
|
@ -14,7 +14,7 @@ abstract class MediaStoreService {
|
|||
|
||||
Future<int?> getGeneration();
|
||||
|
||||
// knownEntries: map of contentId -> dateModifiedSecs
|
||||
// knownEntries: map of contentId -> dateModifiedMillis
|
||||
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory});
|
||||
|
||||
// returns media URI
|
||||
|
|
|
@ -109,7 +109,7 @@ class _EditorImageState extends State<EditorImage> {
|
|||
final viewportSize = margin.deflateSize(constraints.biggest);
|
||||
final minScale = ScaleLevel(factor: ScaleLevel.scaleForContained(viewportSize, canvasSize));
|
||||
return AvesMagnifier(
|
||||
key: Key('${entry.uri}_${entry.pageId}_${entry.dateModifiedSecs}'),
|
||||
key: Key('${entry.uri}_${entry.pageId}_${entry.dateModifiedMillis}'),
|
||||
controller: widget.magnifierController,
|
||||
viewportPadding: margin,
|
||||
contentSize: mediaSize,
|
||||
|
|
|
@ -98,7 +98,7 @@ class ViewerDebugPage extends StatelessWidget {
|
|||
info: {
|
||||
'catalogDateMillis': toDateValue(entry.catalogDateMillis),
|
||||
'dateAddedSecs': toDateValue(entry.dateAddedSecs, factor: 1000),
|
||||
'dateModifiedSecs': toDateValue(entry.dateModifiedSecs, factor: 1000),
|
||||
'dateModifiedMillis': toDateValue(entry.dateModifiedMillis),
|
||||
'sourceDateTakenMillis': toDateValue(entry.sourceDateTakenMillis),
|
||||
'bestDate': '${entry.bestDate}',
|
||||
},
|
||||
|
|
|
@ -398,7 +398,7 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
|
|||
|
||||
return AvesMagnifier(
|
||||
// key includes modified date to refresh when the image is modified by metadata (e.g. rotated)
|
||||
key: Key('${entry.uri}_${entry.pageId}_${entry.dateModifiedSecs}'),
|
||||
key: Key('${entry.uri}_${entry.pageId}_${entry.dateModifiedMillis}'),
|
||||
controller: controller ?? _magnifierController,
|
||||
contentSize: displaySize ?? entry.displaySize,
|
||||
allowOriginalScaleBeyondRange: !isWallpaperMode,
|
||||
|
|
|
@ -40,12 +40,13 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
|
|||
|
||||
static int get nextId => _lastId++;
|
||||
|
||||
static int get dateSecs => DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
static int get dateMillis => DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
static AvesEntry newImage(String album, String filenameWithoutExtension, {int? id, int? contentId}) {
|
||||
id ??= nextId;
|
||||
contentId ??= id;
|
||||
final date = dateSecs;
|
||||
final _dateMillis = dateMillis;
|
||||
final _dateSecs = _dateMillis ~/ 1000;
|
||||
return AvesEntry(
|
||||
origin: EntryOrigins.mediaStoreContent,
|
||||
id: id,
|
||||
|
@ -59,9 +60,9 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
|
|||
sourceRotationDegrees: 0,
|
||||
sizeBytes: 42,
|
||||
sourceTitle: filenameWithoutExtension,
|
||||
dateAddedSecs: date,
|
||||
dateModifiedSecs: date,
|
||||
sourceDateTakenMillis: date,
|
||||
dateAddedSecs: _dateSecs,
|
||||
dateModifiedMillis: _dateMillis,
|
||||
sourceDateTakenMillis: _dateMillis,
|
||||
durationMillis: null,
|
||||
trashed: false,
|
||||
);
|
||||
|
@ -77,7 +78,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
|
|||
EntryFields.uri: 'content://media/external/images/media/$newContentId',
|
||||
EntryFields.contentId: newContentId,
|
||||
EntryFields.path: entry.path!.replaceFirst(sourceAlbum, destinationAlbum),
|
||||
EntryFields.dateModifiedSecs: FakeMediaStoreService.dateSecs,
|
||||
EntryFields.dateModifiedMillis: FakeMediaStoreService.dateMillis,
|
||||
},
|
||||
deleted: false,
|
||||
);
|
||||
|
@ -94,7 +95,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
|
|||
EntryFields.uri: 'content://media/external/images/media/$newContentId',
|
||||
EntryFields.contentId: newContentId,
|
||||
EntryFields.path: entry.path!.replaceFirst(oldName, newName),
|
||||
EntryFields.dateModifiedSecs: FakeMediaStoreService.dateSecs,
|
||||
EntryFields.dateModifiedMillis: FakeMediaStoreService.dateMillis,
|
||||
},
|
||||
deleted: false,
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue