#1427 increased precision of file modified date to milliseconds

This commit is contained in:
Thibault Deckers 2025-02-16 20:32:34 +01:00
parent 9280b4a6a7
commit 5f26cfbbf3
25 changed files with 153 additions and 96 deletions

View file

@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file.
### Changed ### Changed
- increased precision of file modified date to milliseconds
- upgraded Flutter to stable v3.29.0 - upgraded Flutter to stable v3.29.0
### Fixed ### Fixed

View file

@ -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.SvgRegionFetcher
import deckers.thibault.aves.channel.calls.fetchers.ThumbnailFetcher import deckers.thibault.aves.channel.calls.fetchers.ThumbnailFetcher
import deckers.thibault.aves.channel.calls.fetchers.TiffRegionFetcher import deckers.thibault.aves.channel.calls.fetchers.TiffRegionFetcher
import deckers.thibault.aves.model.EntryFields
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel 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) { private suspend fun getThumbnail(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri") val uri = call.argument<String>(EntryFields.URI)
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>(EntryFields.MIME_TYPE)
val dateModifiedSecs = call.argument<Number>("dateModifiedSecs")?.toLong() val dateModifiedMillis = call.argument<Number>(EntryFields.DATE_MODIFIED_MILLIS)?.toLong()
val rotationDegrees = call.argument<Int>("rotationDegrees") val rotationDegrees = call.argument<Int>(EntryFields.ROTATION_DEGREES)
val isFlipped = call.argument<Boolean>("isFlipped") val isFlipped = call.argument<Boolean>(EntryFields.IS_FLIPPED)
val widthDip = call.argument<Number>("widthDip")?.toDouble() val widthDip = call.argument<Number>("widthDip")?.toDouble()
val heightDip = call.argument<Number>("heightDip")?.toDouble() val heightDip = call.argument<Number>("heightDip")?.toDouble()
val pageId = call.argument<Int>("pageId") val pageId = call.argument<Int>("pageId")
@ -55,7 +56,7 @@ class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
context = context, context = context,
uri = uri, uri = uri,
mimeType = mimeType, mimeType = mimeType,
dateModifiedSecs = dateModifiedSecs ?: (Date().time / 1000), dateModifiedMillis = dateModifiedMillis ?: (Date().time),
rotationDegrees = rotationDegrees, rotationDegrees = rotationDegrees,
isFlipped = isFlipped, isFlipped = isFlipped,
width = (widthDip * density).roundToInt(), width = (widthDip * density).roundToInt(),

View file

@ -30,7 +30,7 @@ class ThumbnailFetcher internal constructor(
private val context: Context, private val context: Context,
uri: String, uri: String,
private val mimeType: String, private val mimeType: String,
private val dateModifiedSecs: Long, private val dateModifiedMillis: Long,
private val rotationDegrees: Int, private val rotationDegrees: Int,
private val isFlipped: Boolean, private val isFlipped: Boolean,
width: Int?, 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 // add signature to ignore cache for images which got modified but kept the same URI
var options = RequestOptions() var options = RequestOptions()
.format(if (quality == 100) DecodeFormat.PREFER_ARGB_8888 else DecodeFormat.PREFER_RGB_565) .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) .override(width, height)
if (isVideo(mimeType)) { if (isVideo(mimeType)) {
options = options.diskCacheStrategy(DiskCacheStrategy.RESOURCE) options = options.diskCacheStrategy(DiskCacheStrategy.RESOURCE)

View file

@ -19,12 +19,13 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
private lateinit var eventSink: EventSink private lateinit var eventSink: EventSink
private lateinit var handler: Handler 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 private var directory: String? = null
init { init {
if (arguments is Map<*, *>) { 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? directory = arguments["directory"] as String?
} }
} }

View file

@ -18,7 +18,7 @@ object EntryFields {
const val IS_FLIPPED = "isFlipped" // boolean const val IS_FLIPPED = "isFlipped" // boolean
const val DATE_ADDED_SECS = "dateAddedSecs" // long 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 SOURCE_DATE_TAKEN_MILLIS = "sourceDateTakenMillis" // long
const val DURATION_MILLIS = "durationMillis" // long const val DURATION_MILLIS = "durationMillis" // long

View file

@ -42,7 +42,7 @@ class SourceEntry {
private var sourceRotationDegrees: Int? = null private var sourceRotationDegrees: Int? = null
private var sizeBytes: Long? = null private var sizeBytes: Long? = null
private var dateAddedSecs: Long? = null private var dateAddedSecs: Long? = null
private var dateModifiedSecs: Long? = null private var dateModifiedMillis: Long? = null
private var sourceDateTakenMillis: Long? = null private var sourceDateTakenMillis: Long? = null
private var durationMillis: Long? = null private var durationMillis: Long? = null
@ -65,16 +65,16 @@ class SourceEntry {
sizeBytes = toLong(map[EntryFields.SIZE_BYTES]) sizeBytes = toLong(map[EntryFields.SIZE_BYTES])
title = map[EntryFields.TITLE] as String? title = map[EntryFields.TITLE] as String?
dateAddedSecs = toLong(map[EntryFields.DATE_ADDED_SECS]) 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]) sourceDateTakenMillis = toLong(map[EntryFields.SOURCE_DATE_TAKEN_MILLIS])
durationMillis = toLong(map[EntryFields.DURATION_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.path = path
this.title = title this.title = title
this.sizeBytes = sizeBytes this.sizeBytes = sizeBytes
this.dateModifiedSecs = dateModifiedSecs this.dateModifiedMillis = dateModifiedMillis
} }
fun toMap(): FieldMap { fun toMap(): FieldMap {
@ -89,7 +89,7 @@ class SourceEntry {
EntryFields.SIZE_BYTES to sizeBytes, EntryFields.SIZE_BYTES to sizeBytes,
EntryFields.TITLE to title, EntryFields.TITLE to title,
EntryFields.DATE_ADDED_SECS to dateAddedSecs, 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.SOURCE_DATE_TAKEN_MILLIS to sourceDateTakenMillis,
EntryFields.DURATION_MILLIS to durationMillis, EntryFields.DURATION_MILLIS to durationMillis,
// only for map export // only for map export

View file

@ -46,7 +46,7 @@ internal class FileImageProvider : ImageProvider() {
path = path, path = path,
title = file.name, title = file.name,
sizeBytes = file.length(), sizeBytes = file.length(),
dateModifiedSecs = file.lastModified() / 1000, dateModifiedMillis = file.lastModified(),
) )
} }
} catch (e: SecurityException) { } catch (e: SecurityException) {
@ -91,7 +91,7 @@ internal class FileImageProvider : ImageProvider() {
return hashMapOf( return hashMapOf(
EntryFields.URI to Uri.fromFile(newFile).toString(), EntryFields.URI to Uri.fromFile(newFile).toString(),
EntryFields.PATH to newFile.path, 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 { try {
val file = File(path) val file = File(path)
if (file.exists()) { if (file.exists()) {
newFields[EntryFields.DATE_MODIFIED_SECS] = file.lastModified() / 1000 newFields[EntryFields.DATE_MODIFIED_MILLIS] = file.lastModified()
newFields[EntryFields.SIZE_BYTES] = file.length() newFields[EntryFields.SIZE_BYTES] = file.length()
} }
callback.onSuccess(newFields) callback.onSuccess(newFields)

View file

@ -51,14 +51,14 @@ import kotlin.coroutines.suspendCoroutine
class MediaStoreImageProvider : ImageProvider() { class MediaStoreImageProvider : ImageProvider() {
fun fetchAll( fun fetchAll(
context: Context, context: Context,
knownEntries: Map<Long?, Int?>, knownEntries: Map<Long?, Long?>,
directory: String?, directory: String?,
handleNewEntry: NewEntryHandler, handleNewEntry: NewEntryHandler,
) { ) {
Log.d(LOG_TAG, "fetching all media store items for ${knownEntries.size} known entries, directory=$directory") 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] val knownDate = knownEntries[contentId]
return knownDate == null || knownDate < dateModifiedSecs return knownDate == null || knownDate < dateModifiedMillis
} }
val handleNew: NewEntryHandler val handleNew: NewEntryHandler
var selection: String? = null var selection: String? = null
@ -96,7 +96,7 @@ class MediaStoreImageProvider : ImageProvider() {
var found = false var found = false
val fetched = arrayListOf<FieldMap>() val fetched = arrayListOf<FieldMap>()
val id = uri.tryParseId() 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) } val onSuccess: NewEntryHandler = fun(entry: FieldMap) { fetched.add(entry) }
if (id != null) { if (id != null) {
if (sourceMimeType == null || isImage(sourceMimeType)) { if (sourceMimeType == null || isImage(sourceMimeType)) {
@ -227,8 +227,8 @@ class MediaStoreImageProvider : ImageProvider() {
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE) val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)
val widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH) val widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH)
val heightColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT) val heightColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT)
val dateAddedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED) val dateAddedSecsColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED)
val dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED) val dateModifiedSecsColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)
val dateTakenColumn = cursor.getColumnIndex(MediaColumns.DATE_TAKEN) val dateTakenColumn = cursor.getColumnIndex(MediaColumns.DATE_TAKEN)
// image & video for API >=29, only for images for API <29 // image & video for API >=29, only for images for API <29
@ -240,8 +240,8 @@ class MediaStoreImageProvider : ImageProvider() {
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn) val id = cursor.getLong(idColumn)
val dateModifiedSecs = cursor.getInt(dateModifiedColumn) val dateModifiedMillis = cursor.getInt(dateModifiedSecsColumn) * 1000L
if (isValidEntry(id, dateModifiedSecs)) { if (isValidEntry(id, dateModifiedMillis)) {
// for multiple items, `contentUri` is the root without ID, // for multiple items, `contentUri` is the root without ID,
// but for single items, `contentUri` already contains the ID // but for single items, `contentUri` already contains the ID
val itemUri = if (contentUriContainsId) contentUri else ContentUris.withAppendedId(contentUri, id) val itemUri = if (contentUriContainsId) contentUri else ContentUris.withAppendedId(contentUri, id)
@ -255,17 +255,18 @@ class MediaStoreImageProvider : ImageProvider() {
if (mimeType == null) { if (mimeType == null) {
Log.w(LOG_TAG, "failed to make entry from uri=$itemUri because of null MIME type") Log.w(LOG_TAG, "failed to make entry from uri=$itemUri because of null MIME type")
} else { } else {
var entryMap: FieldMap = hashMapOf( val path = cursor.getString(pathColumn)
var entryFields: FieldMap = hashMapOf(
EntryFields.ORIGIN to SourceEntry.ORIGIN_MEDIA_STORE_CONTENT, EntryFields.ORIGIN to SourceEntry.ORIGIN_MEDIA_STORE_CONTENT,
EntryFields.URI to itemUri.toString(), EntryFields.URI to itemUri.toString(),
EntryFields.PATH to cursor.getString(pathColumn), EntryFields.PATH to path,
EntryFields.SOURCE_MIME_TYPE to mimeType, EntryFields.SOURCE_MIME_TYPE to mimeType,
EntryFields.WIDTH to width, EntryFields.WIDTH to width,
EntryFields.HEIGHT to height, EntryFields.HEIGHT to height,
EntryFields.SOURCE_ROTATION_DEGREES to if (orientationColumn != -1) cursor.getInt(orientationColumn) else 0, EntryFields.SOURCE_ROTATION_DEGREES to if (orientationColumn != -1) cursor.getInt(orientationColumn) else 0,
EntryFields.SIZE_BYTES to cursor.getLong(sizeColumn), EntryFields.SIZE_BYTES to cursor.getLong(sizeColumn),
EntryFields.DATE_ADDED_SECS to cursor.getInt(dateAddedColumn), EntryFields.DATE_ADDED_SECS to cursor.getInt(dateAddedSecsColumn),
EntryFields.DATE_MODIFIED_SECS to dateModifiedSecs, EntryFields.DATE_MODIFIED_MILLIS to dateModifiedMillis,
EntryFields.SOURCE_DATE_TAKEN_MILLIS to if (dateTakenColumn != -1) cursor.getLong(dateTakenColumn) else null, EntryFields.SOURCE_DATE_TAKEN_MILLIS to if (dateTakenColumn != -1) cursor.getLong(dateTakenColumn) else null,
EntryFields.DURATION_MILLIS to durationMillis, EntryFields.DURATION_MILLIS to durationMillis,
// only for map export // only for map export
@ -285,8 +286,8 @@ class MediaStoreImageProvider : ImageProvider() {
if (outWidth > 0 && outHeight > 0) { if (outWidth > 0 && outHeight > 0) {
width = outWidth width = outWidth
height = outHeight height = outHeight
entryMap[EntryFields.WIDTH] = width entryFields[EntryFields.WIDTH] = width
entryMap[EntryFields.HEIGHT] = height entryFields[EntryFields.HEIGHT] = height
} }
} }
} catch (e: IOException) { } catch (e: IOException) {
@ -302,11 +303,13 @@ class MediaStoreImageProvider : ImageProvider() {
// missing some attributes such as width, height, orientation. // missing some attributes such as width, height, orientation.
// Also, the reported size of raw images is inconsistent across devices // Also, the reported size of raw images is inconsistent across devices
// and Android versions (sometimes the raw size, sometimes the decoded size). // and Android versions (sometimes the raw size, sometimes the decoded size).
val entry = SourceEntry(entryMap).fillPreCatalogMetadata(context) val entry = SourceEntry(entryFields).fillPreCatalogMetadata(context)
entryMap = entry.toMap() entryFields = entry.toMap()
} }
handleNewEntry(entryMap) getFileModifiedDateMillis(path)?.let { entryFields[EntryFields.DATE_MODIFIED_MILLIS] = it }
handleNewEntry(entryFields)
found = true found = true
} }
} }
@ -823,18 +826,32 @@ class MediaStoreImageProvider : ImageProvider() {
try { try {
val cursor = context.contentResolver.query(uri, projection, null, null, null) val cursor = context.contentResolver.query(uri, projection, null, null, null)
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(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["sizeBytes"] = cursor.getLong(it) } cursor.getColumnIndex(MediaStore.MediaColumns.SIZE).let { if (it != -1) newFields[EntryFields.SIZE_BYTES] = cursor.getLong(it) }
cursor.close() cursor.close()
} }
} catch (e: Exception) { } catch (e: Exception) {
callback.onFailure(e) callback.onFailure(e)
return@scanFile return@scanFile
} }
getFileModifiedDateMillis(path)?.let { newFields[EntryFields.DATE_MODIFIED_MILLIS] = it }
callback.onSuccess(newFields) 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) { private fun scanObsoletePath(context: Context, uri: Uri, path: String, mimeType: String) {
val file = File(path) val file = File(path)
val delayMillis = 500L val delayMillis = 500L
@ -918,8 +935,9 @@ class MediaStoreImageProvider : ImageProvider() {
EntryFields.PATH to path, 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_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() cursor.close()
getFileModifiedDateMillis(path)?.let { newFields[EntryFields.DATE_MODIFIED_MILLIS] = it }
return newFields return newFields
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -1030,4 +1048,4 @@ object MediaColumns {
typealias NewEntryHandler = (entry: FieldMap) -> Unit typealias NewEntryHandler = (entry: FieldMap) -> Unit
private typealias NewEntryChecker = (contentId: Long, dateModifiedSecs: Int) -> Boolean private typealias NewEntryChecker = (contentId: Long, dateModifiedMillis: Long) -> Boolean

View file

@ -42,7 +42,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
pageId: pageId, pageId: pageId,
rotationDegrees: key.rotationDegrees, rotationDegrees: key.rotationDegrees,
isFlipped: key.isFlipped, isFlipped: key.isFlipped,
dateModifiedSecs: key.dateModifiedSecs, dateModifiedMillis: key.dateModifiedMillis,
extent: key.extent, extent: key.extent,
taskKey: key, taskKey: key,
); );
@ -75,11 +75,11 @@ class ThumbnailProviderKey extends Equatable {
final int? pageId; final int? pageId;
final int rotationDegrees; final int rotationDegrees;
final bool isFlipped; final bool isFlipped;
final int dateModifiedSecs; final int dateModifiedMillis;
final double extent; final double extent;
@override @override
List<Object?> get props => [uri, pageId, dateModifiedSecs, extent]; List<Object?> get props => [uri, pageId, dateModifiedMillis, extent];
const ThumbnailProviderKey({ const ThumbnailProviderKey({
required this.uri, required this.uri,
@ -87,10 +87,10 @@ class ThumbnailProviderKey extends Equatable {
required this.pageId, required this.pageId,
required this.rotationDegrees, required this.rotationDegrees,
required this.isFlipped, required this.isFlipped,
required this.dateModifiedSecs, required this.dateModifiedMillis,
this.extent = 0, this.extent = 0,
}); });
@override @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}';
} }

View file

@ -57,7 +57,7 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
', sizeBytes INTEGER' ', sizeBytes INTEGER'
', title TEXT' ', title TEXT'
', dateAddedSecs INTEGER DEFAULT (strftime(\'%s\',\'now\'))' ', dateAddedSecs INTEGER DEFAULT (strftime(\'%s\',\'now\'))'
', dateModifiedSecs INTEGER' ', dateModifiedMillis INTEGER'
', sourceDateTakenMillis INTEGER' ', sourceDateTakenMillis INTEGER'
', durationMillis INTEGER' ', durationMillis INTEGER'
', trashed INTEGER DEFAULT 0' ', trashed INTEGER DEFAULT 0'
@ -117,7 +117,7 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
')'); ')');
}, },
onUpgrade: LocalMediaDbUpgrader.upgradeDb, onUpgrade: LocalMediaDbUpgrader.upgradeDb,
version: 13, version: 14,
); );
final maxIdRows = await _db.rawQuery('SELECT MAX(id) AS maxId FROM $entryTable'); final maxIdRows = await _db.rawQuery('SELECT MAX(id) AS maxId FROM $entryTable');

View file

@ -47,6 +47,8 @@ class LocalMediaDbUpgrader {
await _upgradeFrom11(db); await _upgradeFrom11(db);
case 12: case 12:
await _upgradeFrom12(db); await _upgradeFrom12(db);
case 13:
await _upgradeFrom13(db);
} }
oldVersion++; oldVersion++;
} }
@ -444,4 +446,37 @@ class LocalMediaDbUpgrader {
await db.execute('ALTER TABLE $newCoverTable RENAME TO $coverTable;'); 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;');
});
}
} }

View file

@ -19,11 +19,11 @@ class EntryCache {
static Future<void> evict( static Future<void> evict(
String uri, String uri,
String mimeType, String mimeType,
int? dateModifiedSecs, int? dateModifiedMillis,
int rotationDegrees, int rotationDegrees,
bool isFlipped, bool isFlipped,
) async { ) 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 // TODO TLAD provide pageId parameter for multi page items, if someday image editing features are added for them
int? pageId; int? pageId;
@ -42,7 +42,7 @@ class EntryCache {
uri: uri, uri: uri,
mimeType: mimeType, mimeType: mimeType,
pageId: pageId, pageId: pageId,
dateModifiedSecs: dateModifiedSecs ?? 0, dateModifiedMillis: dateModifiedMillis ?? 0,
rotationDegrees: rotationDegrees, rotationDegrees: rotationDegrees,
isFlipped: isFlipped, isFlipped: isFlipped,
)).evict(); )).evict();
@ -53,7 +53,7 @@ class EntryCache {
uri: uri, uri: uri,
mimeType: mimeType, mimeType: mimeType,
pageId: pageId, pageId: pageId,
dateModifiedSecs: dateModifiedSecs ?? 0, dateModifiedMillis: dateModifiedMillis ?? 0,
rotationDegrees: rotationDegrees, rotationDegrees: rotationDegrees,
isFlipped: isFlipped, isFlipped: isFlipped,
extent: extent, extent: extent,

View file

@ -36,7 +36,7 @@ class AvesEntry with AvesEntryBase {
int? contentId; int? contentId;
final String sourceMimeType; final String sourceMimeType;
int width, height, sourceRotationDegrees; int width, height, sourceRotationDegrees;
int? dateAddedSecs, _dateModifiedSecs, sourceDateTakenMillis, _durationMillis; int? dateAddedSecs, _dateModifiedMillis, sourceDateTakenMillis, _durationMillis;
bool trashed; bool trashed;
int origin; int origin;
@ -66,7 +66,7 @@ class AvesEntry with AvesEntryBase {
required this.sizeBytes, required this.sizeBytes,
required String? sourceTitle, required String? sourceTitle,
required this.dateAddedSecs, required this.dateAddedSecs,
required int? dateModifiedSecs, required int? dateModifiedMillis,
required this.sourceDateTakenMillis, required this.sourceDateTakenMillis,
required int? durationMillis, required int? durationMillis,
required this.trashed, required this.trashed,
@ -82,7 +82,7 @@ class AvesEntry with AvesEntryBase {
} }
this.path = path; this.path = path;
this.sourceTitle = sourceTitle; this.sourceTitle = sourceTitle;
this.dateModifiedSecs = dateModifiedSecs; this.dateModifiedMillis = dateModifiedMillis;
this.durationMillis = durationMillis; this.durationMillis = durationMillis;
} }
@ -93,7 +93,7 @@ class AvesEntry with AvesEntryBase {
int? contentId, int? contentId,
String? title, String? title,
int? dateAddedSecs, int? dateAddedSecs,
int? dateModifiedSecs, int? dateModifiedMillis,
int? origin, int? origin,
List<AvesEntry>? stackedEntries, List<AvesEntry>? stackedEntries,
}) { }) {
@ -111,7 +111,7 @@ class AvesEntry with AvesEntryBase {
sizeBytes: sizeBytes, sizeBytes: sizeBytes,
sourceTitle: title ?? sourceTitle, sourceTitle: title ?? sourceTitle,
dateAddedSecs: dateAddedSecs ?? this.dateAddedSecs, dateAddedSecs: dateAddedSecs ?? this.dateAddedSecs,
dateModifiedSecs: dateModifiedSecs ?? this.dateModifiedSecs, dateModifiedMillis: dateModifiedMillis ?? this.dateModifiedMillis,
sourceDateTakenMillis: sourceDateTakenMillis, sourceDateTakenMillis: sourceDateTakenMillis,
durationMillis: durationMillis, durationMillis: durationMillis,
trashed: trashed, trashed: trashed,
@ -140,7 +140,7 @@ class AvesEntry with AvesEntryBase {
sizeBytes: map[EntryFields.sizeBytes] as int?, sizeBytes: map[EntryFields.sizeBytes] as int?,
sourceTitle: map[EntryFields.title] as String?, sourceTitle: map[EntryFields.title] as String?,
dateAddedSecs: map[EntryFields.dateAddedSecs] as int?, dateAddedSecs: map[EntryFields.dateAddedSecs] as int?,
dateModifiedSecs: map[EntryFields.dateModifiedSecs] as int?, dateModifiedMillis: map[EntryFields.dateModifiedMillis] as int?,
sourceDateTakenMillis: map[EntryFields.sourceDateTakenMillis] as int?, sourceDateTakenMillis: map[EntryFields.sourceDateTakenMillis] as int?,
durationMillis: map[EntryFields.durationMillis] as int?, durationMillis: map[EntryFields.durationMillis] as int?,
trashed: (map[EntryFields.trashed] as int? ?? 0) != 0, trashed: (map[EntryFields.trashed] as int? ?? 0) != 0,
@ -162,7 +162,7 @@ class AvesEntry with AvesEntryBase {
EntryFields.sizeBytes: sizeBytes, EntryFields.sizeBytes: sizeBytes,
EntryFields.title: sourceTitle, EntryFields.title: sourceTitle,
EntryFields.dateAddedSecs: dateAddedSecs, EntryFields.dateAddedSecs: dateAddedSecs,
EntryFields.dateModifiedSecs: dateModifiedSecs, EntryFields.dateModifiedMillis: dateModifiedMillis,
EntryFields.sourceDateTakenMillis: sourceDateTakenMillis, EntryFields.sourceDateTakenMillis: sourceDateTakenMillis,
EntryFields.durationMillis: durationMillis, EntryFields.durationMillis: durationMillis,
EntryFields.trashed: trashed ? 1 : 0, EntryFields.trashed: trashed ? 1 : 0,
@ -180,7 +180,7 @@ class AvesEntry with AvesEntryBase {
EntryFields.height: height, EntryFields.height: height,
EntryFields.rotationDegrees: rotationDegrees, EntryFields.rotationDegrees: rotationDegrees,
EntryFields.isFlipped: isFlipped, EntryFields.isFlipped: isFlipped,
EntryFields.dateModifiedSecs: dateModifiedSecs, EntryFields.dateModifiedMillis: dateModifiedMillis,
EntryFields.sizeBytes: sizeBytes, EntryFields.sizeBytes: sizeBytes,
EntryFields.trashed: trashed, EntryFields.trashed: trashed,
EntryFields.trashPath: trashDetails?.path, EntryFields.trashPath: trashDetails?.path,
@ -241,7 +241,7 @@ class AvesEntry with AvesEntryBase {
DateTime? _bestDate; DateTime? _bestDate;
DateTime? get bestDate { DateTime? get bestDate {
_bestDate ??= dateTimeFromMillis(_catalogDateMillis) ?? dateTimeFromMillis(sourceDateTakenMillis) ?? dateTimeFromMillis((dateModifiedSecs ?? 0) * 1000); _bestDate ??= dateTimeFromMillis(_catalogDateMillis) ?? dateTimeFromMillis(sourceDateTakenMillis) ?? dateTimeFromMillis(dateModifiedMillis ?? 0);
return _bestDate; return _bestDate;
} }
@ -292,10 +292,10 @@ class AvesEntry with AvesEntryBase {
_bestTitle = null; _bestTitle = null;
} }
int? get dateModifiedSecs => _dateModifiedSecs; int? get dateModifiedMillis => _dateModifiedMillis;
set dateModifiedSecs(int? dateModifiedSecs) { set dateModifiedMillis(int? dateModifiedMillis) {
_dateModifiedSecs = dateModifiedSecs; _dateModifiedMillis = dateModifiedMillis;
_bestDate = null; _bestDate = null;
} }
@ -362,7 +362,7 @@ class AvesEntry with AvesEntryBase {
set catalogMetadata(CatalogMetadata? newMetadata) { set catalogMetadata(CatalogMetadata? newMetadata) {
final oldMimeType = mimeType; final oldMimeType = mimeType;
final oldDateModifiedSecs = dateModifiedSecs; final oldDateModifiedMillis = dateModifiedMillis;
final oldRotationDegrees = rotationDegrees; final oldRotationDegrees = rotationDegrees;
final oldIsFlipped = isFlipped; final oldIsFlipped = isFlipped;
@ -372,7 +372,7 @@ class AvesEntry with AvesEntryBase {
_tags = null; _tags = null;
metadataChangeNotifier.notify(); metadataChangeNotifier.notify();
_onVisualFieldChanged(oldMimeType, oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped); _onVisualFieldChanged(oldMimeType, oldDateModifiedMillis, oldRotationDegrees, oldIsFlipped);
} }
void clearMetadata() { void clearMetadata() {
@ -399,7 +399,7 @@ class AvesEntry with AvesEntryBase {
Future<void> applyNewFields(Map newFields, {required bool persist}) async { Future<void> applyNewFields(Map newFields, {required bool persist}) async {
final oldMimeType = mimeType; final oldMimeType = mimeType;
final oldDateModifiedSecs = this.dateModifiedSecs; final oldDateModifiedMillis = this.dateModifiedMillis;
final oldRotationDegrees = this.rotationDegrees; final oldRotationDegrees = this.rotationDegrees;
final oldIsFlipped = this.isFlipped; final oldIsFlipped = this.isFlipped;
@ -426,8 +426,8 @@ class AvesEntry with AvesEntryBase {
final sizeBytes = newFields[EntryFields.sizeBytes]; final sizeBytes = newFields[EntryFields.sizeBytes];
if (sizeBytes is int) this.sizeBytes = sizeBytes; if (sizeBytes is int) this.sizeBytes = sizeBytes;
final dateModifiedSecs = newFields[EntryFields.dateModifiedSecs]; final dateModifiedMillis = newFields[EntryFields.dateModifiedMillis];
if (dateModifiedSecs is int) this.dateModifiedSecs = dateModifiedSecs; if (dateModifiedMillis is int) this.dateModifiedMillis = dateModifiedMillis;
final rotationDegrees = newFields[EntryFields.rotationDegrees]; final rotationDegrees = newFields[EntryFields.rotationDegrees];
if (rotationDegrees is int) this.rotationDegrees = rotationDegrees; if (rotationDegrees is int) this.rotationDegrees = rotationDegrees;
final isFlipped = newFields[EntryFields.isFlipped]; final isFlipped = newFields[EntryFields.isFlipped];
@ -438,7 +438,7 @@ class AvesEntry with AvesEntryBase {
if (catalogMetadata != null) await localMediaDb.saveCatalogMetadata({catalogMetadata!}); if (catalogMetadata != null) await localMediaDb.saveCatalogMetadata({catalogMetadata!});
} }
await _onVisualFieldChanged(oldMimeType, oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped); await _onVisualFieldChanged(oldMimeType, oldDateModifiedMillis, oldRotationDegrees, oldIsFlipped);
metadataChangeNotifier.notify(); metadataChangeNotifier.notify();
} }
@ -479,12 +479,12 @@ class AvesEntry with AvesEntryBase {
// when the MIME type or the image itself changed (e.g. after rotation) // when the MIME type or the image itself changed (e.g. after rotation)
Future<void> _onVisualFieldChanged( Future<void> _onVisualFieldChanged(
String oldMimeType, String oldMimeType,
int? oldDateModifiedSecs, int? oldDateModifiedMillis,
int oldRotationDegrees, int oldRotationDegrees,
bool oldIsFlipped, bool oldIsFlipped,
) async { ) async {
if ((!MimeTypes.refersToSameType(oldMimeType, mimeType) && !MimeTypes.isVideo(oldMimeType)) || oldDateModifiedSecs != dateModifiedSecs || oldRotationDegrees != rotationDegrees || oldIsFlipped != isFlipped) { if ((!MimeTypes.refersToSameType(oldMimeType, mimeType) && !MimeTypes.isVideo(oldMimeType)) || oldDateModifiedMillis != dateModifiedMillis || oldRotationDegrees != rotationDegrees || oldIsFlipped != isFlipped) {
await EntryCache.evict(uri, oldMimeType, oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped); await EntryCache.evict(uri, oldMimeType, oldDateModifiedMillis, oldRotationDegrees, oldIsFlipped);
visualChangeNotifier.notify(); visualChangeNotifier.notify();
} }
} }

View file

@ -25,7 +25,7 @@ extension ExtraAvesEntryImages on AvesEntry {
pageId: pageId, pageId: pageId,
rotationDegrees: rotationDegrees, rotationDegrees: rotationDegrees,
isFlipped: isFlipped, isFlipped: isFlipped,
dateModifiedSecs: dateModifiedSecs ?? -1, dateModifiedMillis: dateModifiedMillis ?? -1,
extent: requestExtent, extent: requestExtent,
); );
} }

View file

@ -17,7 +17,7 @@ class EntryFields {
static const isFlipped = 'isFlipped'; // boolean static const isFlipped = 'isFlipped'; // boolean
static const dateAddedSecs = 'dateAddedSecs'; // long static const dateAddedSecs = 'dateAddedSecs'; // long
static const dateModifiedSecs = 'dateModifiedSecs'; // long static const dateModifiedMillis = 'dateModifiedMillis'; // long
static const sourceDateTakenMillis = 'sourceDateTakenMillis'; // long static const sourceDateTakenMillis = 'sourceDateTakenMillis'; // long
static const durationMillis = 'durationMillis'; // long static const durationMillis = 'durationMillis'; // long

View file

@ -120,7 +120,7 @@ class MultiPageInfo {
sizeBytes: mainEntry.sizeBytes, sizeBytes: mainEntry.sizeBytes,
sourceTitle: mainEntry.sourceTitle, sourceTitle: mainEntry.sourceTitle,
dateAddedSecs: mainEntry.dateAddedSecs, dateAddedSecs: mainEntry.dateAddedSecs,
dateModifiedSecs: mainEntry.dateModifiedSecs, dateModifiedMillis: mainEntry.dateModifiedMillis,
sourceDateTakenMillis: mainEntry.sourceDateTakenMillis, sourceDateTakenMillis: mainEntry.sourceDateTakenMillis,
durationMillis: pageInfo.durationMillis ?? mainEntry.durationMillis, durationMillis: pageInfo.durationMillis ?? mainEntry.durationMillis,
trashed: trashed, trashed: trashed,

View file

@ -245,10 +245,10 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
switch (key) { switch (key) {
case EntryFields.contentId: case EntryFields.contentId:
entry.contentId = newValue as int?; entry.contentId = newValue as int?;
case EntryFields.dateModifiedSecs: case EntryFields.dateModifiedMillis:
// `dateModifiedSecs` changes when moving entries to another directory, // `dateModifiedMillis` changes when moving entries to another directory,
// but it does not change when renaming the containing directory // but it does not change when renaming the containing directory
entry.dateModifiedSecs = newValue as int?; entry.dateModifiedMillis = newValue as int?;
case EntryFields.path: case EntryFields.path:
entry.path = newValue as String?; entry.path = newValue as String?;
case EntryFields.title: 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 can change when moved files are automatically renamed to avoid conflict
title: newFields[EntryFields.title] as String?, title: newFields[EntryFields.title] as String?,
dateAddedSecs: newFields[EntryFields.dateAddedSecs] as int?, dateAddedSecs: newFields[EntryFields.dateAddedSecs] as int?,
dateModifiedSecs: newFields[EntryFields.dateModifiedSecs] as int?, dateModifiedMillis: newFields[EntryFields.dateModifiedMillis] as int?,
origin: newFields[EntryFields.origin] as int?, origin: newFields[EntryFields.origin] as int?,
)); ));
} else { } else {

View file

@ -101,7 +101,7 @@ class MediaStoreSource extends CollectionSource {
final knownLiveEntries = knownEntries.where((entry) => !entry.trashed).toSet(); final knownLiveEntries = knownEntries.where((entry) => !entry.trashed).toSet();
debugPrint('$runtimeType load ${stopwatch.elapsed} check obsolete entries'); 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 knownContentIds = knownDateByContentId.keys.toList();
final removedContentIds = (await mediaStoreService.checkObsoleteContentIds(knownContentIds)).toSet(); final removedContentIds = (await mediaStoreService.checkObsoleteContentIds(knownContentIds)).toSet();
if (topEntries.isNotEmpty) { if (topEntries.isNotEmpty) {
@ -290,7 +290,7 @@ class MediaStoreSource extends CollectionSource {
if (sourceEntry != null) { if (sourceEntry != null) {
final existingEntry = allEntries.firstWhereOrNull((entry) => entry.contentId == contentId); final existingEntry = allEntries.firstWhereOrNull((entry) => entry.contentId == contentId);
// compare paths because some apps move files without updating their `last modified date` // 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 newPath = sourceEntry.path;
final volume = newPath != null ? androidFileUtils.getStorageVolume(newPath) : null; final volume = newPath != null ? androidFileUtils.getStorageVolume(newPath) : null;
if (volume != null) { if (volume != null) {

View file

@ -225,7 +225,7 @@ class PlatformAppService implements AppService {
pageId: coverEntry.pageId, pageId: coverEntry.pageId,
rotationDegrees: coverEntry.rotationDegrees, rotationDegrees: coverEntry.rotationDegrees,
isFlipped: coverEntry.isFlipped, isFlipped: coverEntry.isFlipped,
dateModifiedSecs: coverEntry.dateModifiedSecs, dateModifiedMillis: coverEntry.dateModifiedMillis,
extent: size, extent: size,
); );
} }

View file

@ -53,7 +53,7 @@ abstract class MediaFetchService {
required int rotationDegrees, required int rotationDegrees,
required int? pageId, required int? pageId,
required bool isFlipped, required bool isFlipped,
required int? dateModifiedSecs, required int? dateModifiedMillis,
required double extent, required double extent,
Object? taskKey, Object? taskKey,
int? priority, int? priority,
@ -211,7 +211,7 @@ class PlatformMediaFetchService implements MediaFetchService {
required int rotationDegrees, required int rotationDegrees,
required int? pageId, required int? pageId,
required bool isFlipped, required bool isFlipped,
required int? dateModifiedSecs, required int? dateModifiedMillis,
required double extent, required double extent,
Object? taskKey, Object? taskKey,
int? priority, int? priority,
@ -222,7 +222,7 @@ class PlatformMediaFetchService implements MediaFetchService {
final result = await _platformBytes.invokeMethod('getThumbnail', <String, dynamic>{ final result = await _platformBytes.invokeMethod('getThumbnail', <String, dynamic>{
'uri': uri, 'uri': uri,
'mimeType': mimeType, 'mimeType': mimeType,
'dateModifiedSecs': dateModifiedSecs, 'dateModifiedMillis': dateModifiedMillis,
'rotationDegrees': rotationDegrees, 'rotationDegrees': rotationDegrees,
'isFlipped': isFlipped, 'isFlipped': isFlipped,
'widthDip': extent, 'widthDip': extent,

View file

@ -14,7 +14,7 @@ abstract class MediaStoreService {
Future<int?> getGeneration(); Future<int?> getGeneration();
// knownEntries: map of contentId -> dateModifiedSecs // knownEntries: map of contentId -> dateModifiedMillis
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory}); Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory});
// returns media URI // returns media URI

View file

@ -109,7 +109,7 @@ class _EditorImageState extends State<EditorImage> {
final viewportSize = margin.deflateSize(constraints.biggest); final viewportSize = margin.deflateSize(constraints.biggest);
final minScale = ScaleLevel(factor: ScaleLevel.scaleForContained(viewportSize, canvasSize)); final minScale = ScaleLevel(factor: ScaleLevel.scaleForContained(viewportSize, canvasSize));
return AvesMagnifier( return AvesMagnifier(
key: Key('${entry.uri}_${entry.pageId}_${entry.dateModifiedSecs}'), key: Key('${entry.uri}_${entry.pageId}_${entry.dateModifiedMillis}'),
controller: widget.magnifierController, controller: widget.magnifierController,
viewportPadding: margin, viewportPadding: margin,
contentSize: mediaSize, contentSize: mediaSize,

View file

@ -98,7 +98,7 @@ class ViewerDebugPage extends StatelessWidget {
info: { info: {
'catalogDateMillis': toDateValue(entry.catalogDateMillis), 'catalogDateMillis': toDateValue(entry.catalogDateMillis),
'dateAddedSecs': toDateValue(entry.dateAddedSecs, factor: 1000), 'dateAddedSecs': toDateValue(entry.dateAddedSecs, factor: 1000),
'dateModifiedSecs': toDateValue(entry.dateModifiedSecs, factor: 1000), 'dateModifiedMillis': toDateValue(entry.dateModifiedMillis),
'sourceDateTakenMillis': toDateValue(entry.sourceDateTakenMillis), 'sourceDateTakenMillis': toDateValue(entry.sourceDateTakenMillis),
'bestDate': '${entry.bestDate}', 'bestDate': '${entry.bestDate}',
}, },

View file

@ -398,7 +398,7 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
return AvesMagnifier( return AvesMagnifier(
// key includes modified date to refresh when the image is modified by metadata (e.g. rotated) // 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, controller: controller ?? _magnifierController,
contentSize: displaySize ?? entry.displaySize, contentSize: displaySize ?? entry.displaySize,
allowOriginalScaleBeyondRange: !isWallpaperMode, allowOriginalScaleBeyondRange: !isWallpaperMode,

View file

@ -40,12 +40,13 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
static int get nextId => _lastId++; 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}) { static AvesEntry newImage(String album, String filenameWithoutExtension, {int? id, int? contentId}) {
id ??= nextId; id ??= nextId;
contentId ??= id; contentId ??= id;
final date = dateSecs; final _dateMillis = dateMillis;
final _dateSecs = _dateMillis ~/ 1000;
return AvesEntry( return AvesEntry(
origin: EntryOrigins.mediaStoreContent, origin: EntryOrigins.mediaStoreContent,
id: id, id: id,
@ -59,9 +60,9 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
sourceRotationDegrees: 0, sourceRotationDegrees: 0,
sizeBytes: 42, sizeBytes: 42,
sourceTitle: filenameWithoutExtension, sourceTitle: filenameWithoutExtension,
dateAddedSecs: date, dateAddedSecs: _dateSecs,
dateModifiedSecs: date, dateModifiedMillis: _dateMillis,
sourceDateTakenMillis: date, sourceDateTakenMillis: _dateMillis,
durationMillis: null, durationMillis: null,
trashed: false, trashed: false,
); );
@ -77,7 +78,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
EntryFields.uri: 'content://media/external/images/media/$newContentId', EntryFields.uri: 'content://media/external/images/media/$newContentId',
EntryFields.contentId: newContentId, EntryFields.contentId: newContentId,
EntryFields.path: entry.path!.replaceFirst(sourceAlbum, destinationAlbum), EntryFields.path: entry.path!.replaceFirst(sourceAlbum, destinationAlbum),
EntryFields.dateModifiedSecs: FakeMediaStoreService.dateSecs, EntryFields.dateModifiedMillis: FakeMediaStoreService.dateMillis,
}, },
deleted: false, deleted: false,
); );
@ -94,7 +95,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
EntryFields.uri: 'content://media/external/images/media/$newContentId', EntryFields.uri: 'content://media/external/images/media/$newContentId',
EntryFields.contentId: newContentId, EntryFields.contentId: newContentId,
EntryFields.path: entry.path!.replaceFirst(oldName, newName), EntryFields.path: entry.path!.replaceFirst(oldName, newName),
EntryFields.dateModifiedSecs: FakeMediaStoreService.dateSecs, EntryFields.dateModifiedMillis: FakeMediaStoreService.dateMillis,
}, },
deleted: false, deleted: false,
); );