diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e2d737c8..2d289ce48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file. - Collection / Info: edit description via Exif / IPTC / XMP - Info: read XMP from HEIC on Android >=11 - Collection: support HEIC motion photos on Android >=11 +- Search: `recently added` filter - Dutch translation (thanks Martijn Fabrie, Koen Koppens) ### Changed diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt index e6f373544..891145aba 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt @@ -1171,7 +1171,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { private const val KEY_LATITUDE = "latitude" private const val KEY_LONGITUDE = "longitude" private const val KEY_XMP_SUBJECTS = "xmpSubjects" - private const val KEY_XMP_TITLE = "xmpTitleDescription" + private const val KEY_XMP_TITLE = "xmpTitle" private const val KEY_RATING = "rating" private const val MASK_IS_ANIMATED = 1 shl 0 diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 40fb1a76a..0028aeeca 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -125,6 +125,7 @@ "filterLocationEmptyLabel": "Unlocated", "filterTagEmptyLabel": "Untagged", "filterOnThisDayLabel": "On this day", + "filterRecentlyAddedLabel": "Recently added", "filterRatingUnratedLabel": "Unrated", "filterRatingRejectedLabel": "Rejected", "filterTypeAnimatedLabel": "Animated", diff --git a/lib/model/db/db_metadata.dart b/lib/model/db/db_metadata.dart index cc23707bf..7f5d70ea4 100644 --- a/lib/model/db/db_metadata.dart +++ b/lib/model/db/db_metadata.dart @@ -10,6 +10,8 @@ import 'package:aves/model/video_playback.dart'; abstract class MetadataDb { int get nextId; + int get timestampSecs; + Future init(); Future dbFileSize(); diff --git a/lib/model/db/db_metadata_sqflite.dart b/lib/model/db/db_metadata_sqflite.dart index 1acb8b5b1..8ad1aa3fe 100644 --- a/lib/model/db/db_metadata_sqflite.dart +++ b/lib/model/db/db_metadata_sqflite.dart @@ -34,6 +34,9 @@ class SqfliteMetadataDb implements MetadataDb { @override int get nextId => ++_lastId; + @override + int get timestampSecs => DateTime.now().millisecondsSinceEpoch ~/ 1000; + @override Future init() async { _db = await openDatabase( @@ -50,6 +53,7 @@ class SqfliteMetadataDb implements MetadataDb { ', sourceRotationDegrees INTEGER' ', sizeBytes INTEGER' ', title TEXT' + ', dateAddedSecs INTEGER DEFAULT (strftime(\'%s\',\'now\'))' ', dateModifiedSecs INTEGER' ', sourceDateTakenMillis INTEGER' ', durationMillis INTEGER' @@ -66,7 +70,7 @@ class SqfliteMetadataDb implements MetadataDb { ', flags INTEGER' ', rotationDegrees INTEGER' ', xmpSubjects TEXT' - ', xmpTitleDescription TEXT' + ', xmpTitle TEXT' ', latitude REAL' ', longitude REAL' ', rating INTEGER' @@ -99,7 +103,7 @@ class SqfliteMetadataDb implements MetadataDb { ')'); }, onUpgrade: MetadataDbUpgrader.upgradeDb, - version: 8, + version: 9, ); final maxIdRows = await _db.rawQuery('SELECT max(id) AS maxId FROM $entryTable'); diff --git a/lib/model/db/db_metadata_sqflite_upgrade.dart b/lib/model/db/db_metadata_sqflite_upgrade.dart index 1ffe9af32..489c6f5c7 100644 --- a/lib/model/db/db_metadata_sqflite_upgrade.dart +++ b/lib/model/db/db_metadata_sqflite_upgrade.dart @@ -38,6 +38,9 @@ class MetadataDbUpgrader { case 7: await _upgradeFrom7(db); break; + case 8: + await _upgradeFrom8(db); + break; } oldVersion++; } @@ -278,4 +281,57 @@ class MetadataDbUpgrader { await db.execute('ALTER TABLE $coverTable ADD COLUMN packageName TEXT;'); await db.execute('ALTER TABLE $coverTable ADD COLUMN color INTEGER;'); } + + static Future _upgradeFrom8(Database db) async { + debugPrint('upgrading DB from v8'); + + // new column `dateAddedSecs` + 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\'))' + ', dateModifiedSecs INTEGER' + ', sourceDateTakenMillis INTEGER' + ', durationMillis INTEGER' + ', trashed INTEGER DEFAULT 0' + ')'); + await db.rawInsert('INSERT INTO $newEntryTable(id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis,trashed)' + ' SELECT id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis,trashed' + ' FROM $entryTable;'); + await db.execute('DROP TABLE $entryTable;'); + await db.execute('ALTER TABLE $newEntryTable RENAME TO $entryTable;'); + }); + + // rename column `xmpTitleDescription` to `xmpTitle` + await db.transaction((txn) async { + const newMetadataTable = '${metadataTable}TEMP'; + await db.execute('CREATE TABLE $newMetadataTable(' + 'id INTEGER PRIMARY KEY' + ', mimeType TEXT' + ', dateMillis INTEGER' + ', flags INTEGER' + ', rotationDegrees INTEGER' + ', xmpSubjects TEXT' + ', xmpTitle TEXT' + ', latitude REAL' + ', longitude REAL' + ', rating INTEGER' + ')'); + await db.rawInsert('INSERT INTO $newMetadataTable(id,mimeType,dateMillis,flags,rotationDegrees,xmpSubjects,xmpTitle,latitude,longitude,rating)' + ' SELECT id,mimeType,dateMillis,flags,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude,rating' + ' FROM $metadataTable;'); + await db.execute('DROP TABLE $metadataTable;'); + await db.execute('ALTER TABLE $newMetadataTable RENAME TO $metadataTable;'); + }); + } } diff --git a/lib/model/entry.dart b/lib/model/entry.dart index 30c928f39..606ac3a09 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -37,7 +37,7 @@ class AvesEntry { int? pageId, contentId; final String sourceMimeType; int width, height, sourceRotationDegrees; - int? sizeBytes, _dateModifiedSecs, sourceDateTakenMillis, _durationMillis; + int? sizeBytes, dateAddedSecs, _dateModifiedSecs, sourceDateTakenMillis, _durationMillis; bool trashed; int? _catalogDateMillis; @@ -61,6 +61,7 @@ class AvesEntry { required this.sourceRotationDegrees, required this.sizeBytes, required String? sourceTitle, + required this.dateAddedSecs, required int? dateModifiedSecs, required this.sourceDateTakenMillis, required int? durationMillis, @@ -83,6 +84,7 @@ class AvesEntry { String? path, int? contentId, String? title, + int? dateAddedSecs, int? dateModifiedSecs, List? burstEntries, }) { @@ -99,6 +101,7 @@ class AvesEntry { sourceRotationDegrees: sourceRotationDegrees, sizeBytes: sizeBytes, sourceTitle: title ?? sourceTitle, + dateAddedSecs: dateAddedSecs ?? this.dateAddedSecs, dateModifiedSecs: dateModifiedSecs ?? this.dateModifiedSecs, sourceDateTakenMillis: sourceDateTakenMillis, durationMillis: durationMillis, @@ -126,6 +129,7 @@ class AvesEntry { sourceRotationDegrees: map['sourceRotationDegrees'] as int? ?? 0, sizeBytes: map['sizeBytes'] as int?, sourceTitle: map['title'] as String?, + dateAddedSecs: map['dateAddedSecs'] as int?, dateModifiedSecs: map['dateModifiedSecs'] as int?, sourceDateTakenMillis: map['sourceDateTakenMillis'] as int?, durationMillis: map['durationMillis'] as int?, @@ -153,6 +157,23 @@ class AvesEntry { }; } + Map toPlatformEntryMap() { + return { + 'uri': uri, + 'path': path, + 'pageId': pageId, + 'mimeType': mimeType, + 'width': width, + 'height': height, + 'rotationDegrees': rotationDegrees, + 'isFlipped': isFlipped, + 'dateModifiedSecs': dateModifiedSecs, + 'sizeBytes': sizeBytes, + 'trashed': trashed, + 'trashPath': trashDetails?.path, + }; + } + void dispose() { imageChangeNotifier.dispose(); metadataChangeNotifier.dispose(); @@ -464,7 +485,7 @@ class AvesEntry { String? _bestTitle; String? get bestTitle { - _bestTitle ??= _catalogMetadata?.xmpTitleDescription?.isNotEmpty == true ? _catalogMetadata!.xmpTitleDescription : (filenameWithoutExtension ?? sourceTitle); + _bestTitle ??= _catalogMetadata?.xmpTitle?.isNotEmpty == true ? _catalogMetadata!.xmpTitle : (filenameWithoutExtension ?? sourceTitle); return _bestTitle; } diff --git a/lib/model/filters/filters.dart b/lib/model/filters/filters.dart index f0787538c..4fdd2822a 100644 --- a/lib/model/filters/filters.dart +++ b/lib/model/filters/filters.dart @@ -11,6 +11,7 @@ import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/path.dart'; import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/rating.dart'; +import 'package:aves/model/filters/recent.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/filters/trash.dart'; import 'package:aves/model/filters/type.dart'; @@ -68,6 +69,8 @@ abstract class CollectionFilter extends Equatable implements Comparable get props => []; + + RecentlyAddedFilter._private() { + updateNow(); + } + + @override + Map toMap() => { + 'type': type, + }; + + @override + EntryFilter get test => (entry) => (nowSecs - (entry.dateAddedSecs ?? 0)) < _dayInSecs; + + @override + String get universalLabel => type; + + @override + String getLabel(BuildContext context) => context.l10n.filterRecentlyAddedLabel; + + @override + Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.recent, size: size); + + @override + String get category => type; + + @override + String get key => type; +} diff --git a/lib/model/metadata/catalog.dart b/lib/model/metadata/catalog.dart index 0b75a69a0..ee09e90b0 100644 --- a/lib/model/metadata/catalog.dart +++ b/lib/model/metadata/catalog.dart @@ -7,7 +7,7 @@ class CatalogMetadata { final bool isAnimated, isGeotiff, is360, isMultiPage, isMotionPhoto; bool isFlipped; int? rotationDegrees; - final String? mimeType, xmpSubjects, xmpTitleDescription; + final String? mimeType, xmpSubjects, xmpTitle; double? latitude, longitude; Address? address; int rating; @@ -32,7 +32,7 @@ class CatalogMetadata { this.isMotionPhoto = false, this.rotationDegrees, this.xmpSubjects, - this.xmpTitleDescription, + this.xmpTitle, double? latitude, double? longitude, this.rating = 0, @@ -72,7 +72,7 @@ class CatalogMetadata { isMotionPhoto: isMotionPhoto, rotationDegrees: rotationDegrees ?? this.rotationDegrees, xmpSubjects: xmpSubjects, - xmpTitleDescription: xmpTitleDescription, + xmpTitle: xmpTitle, latitude: latitude ?? this.latitude, longitude: longitude ?? this.longitude, rating: rating, @@ -94,7 +94,7 @@ class CatalogMetadata { // `rotationDegrees` should default to `sourceRotationDegrees`, not 0 rotationDegrees: map['rotationDegrees'], xmpSubjects: map['xmpSubjects'] ?? '', - xmpTitleDescription: map['xmpTitleDescription'] ?? '', + xmpTitle: map['xmpTitle'] ?? '', latitude: map['latitude'], longitude: map['longitude'], rating: map['rating'] ?? 0, @@ -108,12 +108,12 @@ class CatalogMetadata { 'flags': (isAnimated ? _isAnimatedMask : 0) | (isFlipped ? _isFlippedMask : 0) | (isGeotiff ? _isGeotiffMask : 0) | (is360 ? _is360Mask : 0) | (isMultiPage ? _isMultiPageMask : 0) | (isMotionPhoto ? _isMotionPhotoMask : 0), 'rotationDegrees': rotationDegrees, 'xmpSubjects': xmpSubjects, - 'xmpTitleDescription': xmpTitleDescription, + 'xmpTitle': xmpTitle, 'latitude': latitude, 'longitude': longitude, 'rating': rating, }; @override - String toString() => '$runtimeType#${shortHash(this)}{id=$id, mimeType=$mimeType, dateMillis=$dateMillis, isAnimated=$isAnimated, isFlipped=$isFlipped, isGeotiff=$isGeotiff, is360=$is360, isMultiPage=$isMultiPage, isMotionPhoto=$isMotionPhoto, rotationDegrees=$rotationDegrees, xmpSubjects=$xmpSubjects, xmpTitleDescription=$xmpTitleDescription, latitude=$latitude, longitude=$longitude, rating=$rating}'; + String toString() => '$runtimeType#${shortHash(this)}{id=$id, mimeType=$mimeType, dateMillis=$dateMillis, isAnimated=$isAnimated, isFlipped=$isFlipped, isGeotiff=$isGeotiff, is360=$is360, isMultiPage=$isMultiPage, isMotionPhoto=$isMotionPhoto, rotationDegrees=$rotationDegrees, xmpSubjects=$xmpSubjects, xmpTitle=$xmpTitle, latitude=$latitude, longitude=$longitude, rating=$rating}'; } diff --git a/lib/model/multipage.dart b/lib/model/multipage.dart index 6e3ad53d8..e0d8159ce 100644 --- a/lib/model/multipage.dart +++ b/lib/model/multipage.dart @@ -102,6 +102,7 @@ class MultiPageInfo { sourceRotationDegrees: pageInfo.rotationDegrees ?? mainEntry.sourceRotationDegrees, sizeBytes: mainEntry.sizeBytes, sourceTitle: mainEntry.sourceTitle, + dateAddedSecs: mainEntry.dateAddedSecs, dateModifiedSecs: mainEntry.dateModifiedSecs, sourceDateTakenMillis: mainEntry.sourceDateTakenMillis, durationMillis: pageInfo.durationMillis ?? mainEntry.durationMillis, diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index 98db16d85..0248fd48c 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -2,6 +2,7 @@ import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry_set_actions.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/mime.dart'; +import 'package:aves/model/filters/recent.dart'; import 'package:aves/model/naming_pattern.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/source/enums.dart'; @@ -41,6 +42,7 @@ class SettingsDefaults { null, MimeFilter.video, FavouriteFilter.instance, + RecentlyAddedFilter.instance, ]; static const drawerPageBookmarks = [ AlbumListPage.routeName, diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index 3ba0bca9e..af84abb28 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -294,6 +294,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM contentId: newFields['contentId'] as int?, // title can change when moved files are automatically renamed to avoid conflict title: newFields['title'] as String?, + dateAddedSecs: metadataDb.timestampSecs, dateModifiedSecs: newFields['dateModifiedSecs'] as int?, )); } else { diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index 47eb06685..481804577 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -158,7 +158,14 @@ class MediaStoreSource extends CollectionSource { // when discovering modified entry with known content ID, // reuse known entry ID to overwrite it while preserving favourites, etc. final contentId = entry.contentId; - entry.id = (knownContentIds.contains(contentId) ? knownLiveEntries.firstWhereOrNull((entry) => entry.contentId == contentId)?.id : null) ?? metadataDb.nextId; + final existingEntry = knownContentIds.contains(contentId) ? knownLiveEntries.firstWhereOrNull((entry) => entry.contentId == contentId) : null; + if (existingEntry != null) { + entry.id = existingEntry.id; + entry.dateAddedSecs = existingEntry.dateAddedSecs; + } else { + entry.id = metadataDb.nextId; + entry.dateAddedSecs = metadataDb.timestampSecs; + } pendingNewEntries.add(entry); if (pendingNewEntries.length >= refreshCount) { @@ -243,7 +250,13 @@ class MediaStoreSource extends CollectionSource { final newPath = sourceEntry.path; final volume = newPath != null ? androidFileUtils.getStorageVolume(newPath) : null; if (volume != null) { - sourceEntry.id = existingEntry?.id ?? metadataDb.nextId; + if (existingEntry != null) { + sourceEntry.id = existingEntry.id; + sourceEntry.dateAddedSecs = existingEntry.dateAddedSecs; + } else { + sourceEntry.id = metadataDb.nextId; + sourceEntry.dateAddedSecs = metadataDb.timestampSecs; + } newEntries.add(sourceEntry); final existingDirectory = existingEntry?.directory; if (existingDirectory != null) { diff --git a/lib/services/media/media_edit_service.dart b/lib/services/media/media_edit_service.dart index 00b7788f2..a775c827d 100644 --- a/lib/services/media/media_edit_service.dart +++ b/lib/services/media/media_edit_service.dart @@ -52,23 +52,6 @@ class PlatformMediaEditService implements MediaEditService { static const _platform = MethodChannel('deckers.thibault/aves/media_edit'); static final _opStream = StreamsChannel('deckers.thibault/aves/media_op_stream'); - static Map _toPlatformEntryMap(AvesEntry entry) { - return { - 'uri': entry.uri, - 'path': entry.path, - 'pageId': entry.pageId, - 'mimeType': entry.mimeType, - 'width': entry.width, - 'height': entry.height, - 'rotationDegrees': entry.rotationDegrees, - 'isFlipped': entry.isFlipped, - 'dateModifiedSecs': entry.dateModifiedSecs, - 'sizeBytes': entry.sizeBytes, - 'trashed': entry.trashed, - 'trashPath': entry.trashDetails?.path, - }; - } - @override String get newOpId => DateTime.now().millisecondsSinceEpoch.toString(); @@ -93,7 +76,7 @@ class PlatformMediaEditService implements MediaEditService { .receiveBroadcastStream({ 'op': 'delete', 'id': opId, - 'entries': entries.map(_toPlatformEntryMap).toList(), + 'entries': entries.map((entry) => entry.toPlatformEntryMap()).toList(), }) .where((event) => event is Map) .map((event) => ImageOpEvent.fromMap(event as Map)); @@ -115,7 +98,7 @@ class PlatformMediaEditService implements MediaEditService { .receiveBroadcastStream({ 'op': 'move', 'id': opId, - 'entriesByDestination': entriesByDestination.map((destination, entries) => MapEntry(destination, entries.map(_toPlatformEntryMap).toList())), + 'entriesByDestination': entriesByDestination.map((destination, entries) => MapEntry(destination, entries.map((entry) => entry.toPlatformEntryMap()).toList())), 'copy': copy, 'nameConflictStrategy': nameConflictStrategy.toPlatform(), }) @@ -138,7 +121,7 @@ class PlatformMediaEditService implements MediaEditService { return _opStream .receiveBroadcastStream({ 'op': 'export', - 'entries': entries.map(_toPlatformEntryMap).toList(), + 'entries': entries.map((entry) => entry.toPlatformEntryMap()).toList(), 'mimeType': options.mimeType, 'width': options.width, 'height': options.height, @@ -163,7 +146,7 @@ class PlatformMediaEditService implements MediaEditService { .receiveBroadcastStream({ 'op': 'rename', 'id': opId, - 'entriesToNewName': entriesToNewName.map((key, value) => MapEntry(_toPlatformEntryMap(key), value)), + 'entriesToNewName': entriesToNewName.map((entry, name) => MapEntry(entry.toPlatformEntryMap(), name)), }) .where((event) => event is Map) .map((event) => MoveOpEvent.fromMap(event as Map)); diff --git a/lib/services/metadata/metadata_edit_service.dart b/lib/services/metadata/metadata_edit_service.dart index 6cd48fae4..448c60bca 100644 --- a/lib/services/metadata/metadata_edit_service.dart +++ b/lib/services/metadata/metadata_edit_service.dart @@ -25,27 +25,12 @@ abstract class MetadataEditService { class PlatformMetadataEditService implements MetadataEditService { static const _platform = MethodChannel('deckers.thibault/aves/metadata_edit'); - static Map _toPlatformEntryMap(AvesEntry entry) { - return { - 'uri': entry.uri, - 'path': entry.path, - 'pageId': entry.pageId, - 'mimeType': entry.mimeType, - 'width': entry.width, - 'height': entry.height, - 'rotationDegrees': entry.rotationDegrees, - 'isFlipped': entry.isFlipped, - 'dateModifiedSecs': entry.dateModifiedSecs, - 'sizeBytes': entry.sizeBytes, - }; - } - @override Future> rotate(AvesEntry entry, {required bool clockwise}) async { try { // returns map with: 'rotationDegrees' 'isFlipped' final result = await _platform.invokeMethod('rotate', { - 'entry': _toPlatformEntryMap(entry), + 'entry': entry.toPlatformEntryMap(), 'clockwise': clockwise, }); if (result != null) return (result as Map).cast(); @@ -62,7 +47,7 @@ class PlatformMetadataEditService implements MetadataEditService { try { // returns map with: 'rotationDegrees' 'isFlipped' final result = await _platform.invokeMethod('flip', { - 'entry': _toPlatformEntryMap(entry), + 'entry': entry.toPlatformEntryMap(), }); if (result != null) return (result as Map).cast(); } on PlatformException catch (e, stack) { @@ -77,7 +62,7 @@ class PlatformMetadataEditService implements MetadataEditService { Future> editExifDate(AvesEntry entry, DateModifier modifier) async { try { final result = await _platform.invokeMethod('editDate', { - 'entry': _toPlatformEntryMap(entry), + 'entry': entry.toPlatformEntryMap(), 'dateMillis': modifier.setDateTime?.millisecondsSinceEpoch, 'shiftMinutes': modifier.shiftMinutes, 'fields': modifier.fields.where((v) => v.type == MetadataType.exif).map((v) => v.exifInterfaceTag).whereNotNull().toList(), @@ -99,7 +84,7 @@ class PlatformMetadataEditService implements MetadataEditService { }) async { try { final result = await _platform.invokeMethod('editMetadata', { - 'entry': _toPlatformEntryMap(entry), + 'entry': entry.toPlatformEntryMap(), 'metadata': metadata.map((type, value) => MapEntry(_toPlatformMetadataType(type), value)), 'autoCorrectTrailerOffset': autoCorrectTrailerOffset, }); @@ -116,7 +101,7 @@ class PlatformMetadataEditService implements MetadataEditService { Future> removeTrailerVideo(AvesEntry entry) async { try { final result = await _platform.invokeMethod('removeTrailerVideo', { - 'entry': _toPlatformEntryMap(entry), + 'entry': entry.toPlatformEntryMap(), }); if (result != null) return (result as Map).cast(); } on PlatformException catch (e, stack) { @@ -131,7 +116,7 @@ class PlatformMetadataEditService implements MetadataEditService { Future> removeTypes(AvesEntry entry, Set types) async { try { final result = await _platform.invokeMethod('removeTypes', { - 'entry': _toPlatformEntryMap(entry), + 'entry': entry.toPlatformEntryMap(), 'types': types.map(_toPlatformMetadataType).toList(), }); if (result != null) return (result as Map).cast(); diff --git a/lib/services/metadata/metadata_fetch_service.dart b/lib/services/metadata/metadata_fetch_service.dart index 527cabc97..041d41383 100644 --- a/lib/services/metadata/metadata_fetch_service.dart +++ b/lib/services/metadata/metadata_fetch_service.dart @@ -77,7 +77,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { // 'latitude': latitude (double) // 'longitude': longitude (double) // 'xmpSubjects': ';' separated XMP subjects (string) - // 'xmpTitleDescription': XMP title (string) + // 'xmpTitle': XMP title (string) final result = await _platform.invokeMethod('getCatalogMetadata', { 'mimeType': entry.mimeType, 'uri': entry.uri, diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index 5fb0e56de..922ef21ef 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -35,6 +35,7 @@ class AIcons { static const IconData ratingRejected = MdiIcons.starMinusOutline; static const IconData ratingUnrated = MdiIcons.starOffOutline; static const IconData raw = Icons.raw_on_outlined; + static const IconData recent = Icons.today_outlined; static const IconData shooting = Icons.camera_outlined; static const IconData removableStorage = Icons.sd_storage_outlined; static const IconData sensorControlEnabled = Icons.explore_outlined; diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index 6cb98f706..b9cf72735 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -5,6 +5,7 @@ import 'package:aves/app_flavor.dart'; import 'package:aves/app_mode.dart'; import 'package:aves/l10n/l10n.dart'; import 'package:aves/model/device.dart'; +import 'package:aves/model/filters/recent.dart'; import 'package:aves/model/settings/defaults.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/enums/display_refresh_rate_mode.dart'; @@ -275,9 +276,11 @@ class _AvesAppState extends State with WidgetsBindingObserver { break; } break; + case AppLifecycleState.resumed: + RecentlyAddedFilter.updateNow(); + break; case AppLifecycleState.paused: case AppLifecycleState.detached: - case AppLifecycleState.resumed: break; } } diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart index 6b0793035..0bd3980dc 100644 --- a/lib/widgets/search/search_delegate.dart +++ b/lib/widgets/search/search_delegate.dart @@ -6,6 +6,7 @@ import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/rating.dart'; +import 'package:aves/model/filters/recent.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/filters/type.dart'; import 'package:aves/model/settings/settings.dart'; @@ -130,6 +131,7 @@ class CollectionSearchDelegate extends AvesSearchDelegate { Widget _buildDateFilters(BuildContext context, _ContainQuery containQuery) { final filters = [ DateFilter.onThisDay, + RecentlyAddedFilter.instance, ..._monthFilters, ].where((f) => containQuery(f.getLabel(context))).toList(); return _buildFilterRow( diff --git a/lib/widgets/settings/navigation/drawer.dart b/lib/widgets/settings/navigation/drawer.dart index e3dfe27d8..2e8d59e06 100644 --- a/lib/widgets/settings/navigation/drawer.dart +++ b/lib/widgets/settings/navigation/drawer.dart @@ -1,4 +1,5 @@ import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/filters/recent.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; @@ -31,6 +32,7 @@ class _NavigationDrawerEditorPageState extends State static final Set _typeOptions = { null, + RecentlyAddedFilter.instance, ...CollectionSearchDelegate.typeFilters, }; static const Set _pageOptions = { diff --git a/lib/widgets/viewer/debug/db.dart b/lib/widgets/viewer/debug/db.dart index 95b5d2fc7..f74a163d0 100644 --- a/lib/widgets/viewer/debug/db.dart +++ b/lib/widgets/viewer/debug/db.dart @@ -94,6 +94,7 @@ class _DbTabState extends State { 'sourceRotationDegrees': '${data.sourceRotationDegrees}', 'sizeBytes': '${data.sizeBytes}', 'sourceTitle': data.sourceTitle ?? '', + 'dateAddedSecs': '${data.dateAddedSecs}', 'dateModifiedSecs': '${data.dateModifiedSecs}', 'sourceDateTakenMillis': '${data.sourceDateTakenMillis}', 'durationMillis': '${data.durationMillis}', @@ -126,7 +127,7 @@ class _DbTabState extends State { 'latitude': '${data.latitude}', 'longitude': '${data.longitude}', 'xmpSubjects': data.xmpSubjects ?? '', - 'xmpTitleDescription': data.xmpTitleDescription ?? '', + 'xmpTitle': data.xmpTitle ?? '', 'rating': '${data.rating}', }, ), diff --git a/lib/widgets/viewer/debug/debug_page.dart b/lib/widgets/viewer/debug/debug_page.dart index 7a00847ee..589462ba8 100644 --- a/lib/widgets/viewer/debug/debug_page.dart +++ b/lib/widgets/viewer/debug/debug_page.dart @@ -98,6 +98,7 @@ class ViewerDebugPage extends StatelessWidget { InfoRowGroup( info: { 'catalogDateMillis': toDateValue(entry.catalogDateMillis), + 'dateAddedSecs': toDateValue(entry.dateAddedSecs, factor: 1000), 'dateModifiedSecs': toDateValue(entry.dateModifiedSecs, factor: 1000), 'sourceDateTakenMillis': toDateValue(entry.sourceDateTakenMillis), 'bestDate': '${entry.bestDate}', diff --git a/test/fake/media_store_service.dart b/test/fake/media_store_service.dart index e6172611e..1ef2e2bb9 100644 --- a/test/fake/media_store_service.dart +++ b/test/fake/media_store_service.dart @@ -38,6 +38,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService { sourceRotationDegrees: 0, sizeBytes: 42, sourceTitle: filenameWithoutExtension, + dateAddedSecs: date, dateModifiedSecs: date, sourceDateTakenMillis: date, durationMillis: null, diff --git a/test/model/filters_test.dart b/test/model/filters_test.dart index d1107457a..f6d7059d8 100644 --- a/test/model/filters_test.dart +++ b/test/model/filters_test.dart @@ -8,6 +8,7 @@ import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/path.dart'; import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/rating.dart'; +import 'package:aves/model/filters/recent.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/filters/type.dart'; import 'package:aves/services/common/services.dart'; @@ -61,6 +62,9 @@ void main() { const rating = RatingFilter(3); expect(rating, jsonRoundTrip(rating)); + final recent = RecentlyAddedFilter.instance; + expect(recent, jsonRoundTrip(recent)); + final tag = TagFilter('some tag'); expect(tag, jsonRoundTrip(tag)); diff --git a/untranslated.json b/untranslated.json index cd1379c74..9158ee212 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,6 +1,7 @@ { "de": [ "entryInfoActionEditDescription", + "filterRecentlyAddedLabel", "editEntryDescriptionDialogTitle", "settingsConfirmationAfterMoveToBinItems", "settingsViewerGestureSideTapNext" @@ -8,18 +9,21 @@ "es": [ "entryInfoActionEditDescription", + "filterRecentlyAddedLabel", "editEntryDescriptionDialogTitle", "settingsConfirmationAfterMoveToBinItems" ], "fr": [ "entryInfoActionEditDescription", + "filterRecentlyAddedLabel", "editEntryDescriptionDialogTitle", "settingsViewerGestureSideTapNext" ], "id": [ "entryInfoActionEditDescription", + "filterRecentlyAddedLabel", "editEntryDescriptionDialogTitle", "settingsConfirmationAfterMoveToBinItems", "settingsViewerGestureSideTapNext" @@ -27,6 +31,7 @@ "it": [ "entryInfoActionEditDescription", + "filterRecentlyAddedLabel", "editEntryDescriptionDialogTitle", "settingsConfirmationAfterMoveToBinItems", "settingsViewerGestureSideTapNext" @@ -34,6 +39,7 @@ "ja": [ "entryInfoActionEditDescription", + "filterRecentlyAddedLabel", "editEntryDescriptionDialogTitle", "settingsConfirmationAfterMoveToBinItems", "settingsViewerGestureSideTapNext" @@ -41,12 +47,14 @@ "ko": [ "entryInfoActionEditDescription", + "filterRecentlyAddedLabel", "editEntryDescriptionDialogTitle", "settingsViewerGestureSideTapNext" ], "nl": [ "entryInfoActionEditDescription", + "filterRecentlyAddedLabel", "editEntryDescriptionDialogTitle", "settingsConfirmationAfterMoveToBinItems", "settingsViewerGestureSideTapNext", @@ -55,6 +63,7 @@ "pt": [ "entryInfoActionEditDescription", + "filterRecentlyAddedLabel", "editEntryDescriptionDialogTitle", "settingsConfirmationAfterMoveToBinItems", "settingsViewerGestureSideTapNext" @@ -63,6 +72,7 @@ "ru": [ "entryInfoActionEditDescription", "filterOnThisDayLabel", + "filterRecentlyAddedLabel", "editEntryDescriptionDialogTitle", "settingsConfirmationAfterMoveToBinItems", "settingsViewerGestureSideTapNext", @@ -77,6 +87,7 @@ "slideshowActionShowInCollection", "entryInfoActionEditDescription", "filterOnThisDayLabel", + "filterRecentlyAddedLabel", "slideshowVideoPlaybackSkip", "slideshowVideoPlaybackMuted", "slideshowVideoPlaybackWithSound", @@ -110,6 +121,7 @@ "zh": [ "entryInfoActionEditDescription", + "filterRecentlyAddedLabel", "editEntryDescriptionDialogTitle", "settingsConfirmationAfterMoveToBinItems", "settingsViewerGestureSideTapNext"