#97 search: recently added filter

This commit is contained in:
Thibault Deckers 2022-08-24 10:53:59 +02:00
parent 0cce0c1e11
commit 503f93ed17
26 changed files with 206 additions and 58 deletions

View file

@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file.
- Collection / Info: edit description via Exif / IPTC / XMP - Collection / Info: edit description via Exif / IPTC / XMP
- Info: read XMP from HEIC on Android >=11 - Info: read XMP from HEIC on Android >=11
- Collection: support HEIC motion photos on Android >=11 - Collection: support HEIC motion photos on Android >=11
- Search: `recently added` filter
- Dutch translation (thanks Martijn Fabrie, Koen Koppens) - Dutch translation (thanks Martijn Fabrie, Koen Koppens)
### Changed ### Changed

View file

@ -1171,7 +1171,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
private const val KEY_LATITUDE = "latitude" private const val KEY_LATITUDE = "latitude"
private const val KEY_LONGITUDE = "longitude" private const val KEY_LONGITUDE = "longitude"
private const val KEY_XMP_SUBJECTS = "xmpSubjects" 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 KEY_RATING = "rating"
private const val MASK_IS_ANIMATED = 1 shl 0 private const val MASK_IS_ANIMATED = 1 shl 0

View file

@ -125,6 +125,7 @@
"filterLocationEmptyLabel": "Unlocated", "filterLocationEmptyLabel": "Unlocated",
"filterTagEmptyLabel": "Untagged", "filterTagEmptyLabel": "Untagged",
"filterOnThisDayLabel": "On this day", "filterOnThisDayLabel": "On this day",
"filterRecentlyAddedLabel": "Recently added",
"filterRatingUnratedLabel": "Unrated", "filterRatingUnratedLabel": "Unrated",
"filterRatingRejectedLabel": "Rejected", "filterRatingRejectedLabel": "Rejected",
"filterTypeAnimatedLabel": "Animated", "filterTypeAnimatedLabel": "Animated",

View file

@ -10,6 +10,8 @@ import 'package:aves/model/video_playback.dart';
abstract class MetadataDb { abstract class MetadataDb {
int get nextId; int get nextId;
int get timestampSecs;
Future<void> init(); Future<void> init();
Future<int> dbFileSize(); Future<int> dbFileSize();

View file

@ -34,6 +34,9 @@ class SqfliteMetadataDb implements MetadataDb {
@override @override
int get nextId => ++_lastId; int get nextId => ++_lastId;
@override
int get timestampSecs => DateTime.now().millisecondsSinceEpoch ~/ 1000;
@override @override
Future<void> init() async { Future<void> init() async {
_db = await openDatabase( _db = await openDatabase(
@ -50,6 +53,7 @@ class SqfliteMetadataDb implements MetadataDb {
', sourceRotationDegrees INTEGER' ', sourceRotationDegrees INTEGER'
', sizeBytes INTEGER' ', sizeBytes INTEGER'
', title TEXT' ', title TEXT'
', dateAddedSecs INTEGER DEFAULT (strftime(\'%s\',\'now\'))'
', dateModifiedSecs INTEGER' ', dateModifiedSecs INTEGER'
', sourceDateTakenMillis INTEGER' ', sourceDateTakenMillis INTEGER'
', durationMillis INTEGER' ', durationMillis INTEGER'
@ -66,7 +70,7 @@ class SqfliteMetadataDb implements MetadataDb {
', flags INTEGER' ', flags INTEGER'
', rotationDegrees INTEGER' ', rotationDegrees INTEGER'
', xmpSubjects TEXT' ', xmpSubjects TEXT'
', xmpTitleDescription TEXT' ', xmpTitle TEXT'
', latitude REAL' ', latitude REAL'
', longitude REAL' ', longitude REAL'
', rating INTEGER' ', rating INTEGER'
@ -99,7 +103,7 @@ class SqfliteMetadataDb implements MetadataDb {
')'); ')');
}, },
onUpgrade: MetadataDbUpgrader.upgradeDb, onUpgrade: MetadataDbUpgrader.upgradeDb,
version: 8, version: 9,
); );
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

@ -38,6 +38,9 @@ class MetadataDbUpgrader {
case 7: case 7:
await _upgradeFrom7(db); await _upgradeFrom7(db);
break; break;
case 8:
await _upgradeFrom8(db);
break;
} }
oldVersion++; 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 packageName TEXT;');
await db.execute('ALTER TABLE $coverTable ADD COLUMN color INTEGER;'); await db.execute('ALTER TABLE $coverTable ADD COLUMN color INTEGER;');
} }
static Future<void> _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;');
});
}
} }

View file

@ -37,7 +37,7 @@ class AvesEntry {
int? pageId, contentId; int? pageId, contentId;
final String sourceMimeType; final String sourceMimeType;
int width, height, sourceRotationDegrees; int width, height, sourceRotationDegrees;
int? sizeBytes, _dateModifiedSecs, sourceDateTakenMillis, _durationMillis; int? sizeBytes, dateAddedSecs, _dateModifiedSecs, sourceDateTakenMillis, _durationMillis;
bool trashed; bool trashed;
int? _catalogDateMillis; int? _catalogDateMillis;
@ -61,6 +61,7 @@ class AvesEntry {
required this.sourceRotationDegrees, required this.sourceRotationDegrees,
required this.sizeBytes, required this.sizeBytes,
required String? sourceTitle, required String? sourceTitle,
required this.dateAddedSecs,
required int? dateModifiedSecs, required int? dateModifiedSecs,
required this.sourceDateTakenMillis, required this.sourceDateTakenMillis,
required int? durationMillis, required int? durationMillis,
@ -83,6 +84,7 @@ class AvesEntry {
String? path, String? path,
int? contentId, int? contentId,
String? title, String? title,
int? dateAddedSecs,
int? dateModifiedSecs, int? dateModifiedSecs,
List<AvesEntry>? burstEntries, List<AvesEntry>? burstEntries,
}) { }) {
@ -99,6 +101,7 @@ class AvesEntry {
sourceRotationDegrees: sourceRotationDegrees, sourceRotationDegrees: sourceRotationDegrees,
sizeBytes: sizeBytes, sizeBytes: sizeBytes,
sourceTitle: title ?? sourceTitle, sourceTitle: title ?? sourceTitle,
dateAddedSecs: dateAddedSecs ?? this.dateAddedSecs,
dateModifiedSecs: dateModifiedSecs ?? this.dateModifiedSecs, dateModifiedSecs: dateModifiedSecs ?? this.dateModifiedSecs,
sourceDateTakenMillis: sourceDateTakenMillis, sourceDateTakenMillis: sourceDateTakenMillis,
durationMillis: durationMillis, durationMillis: durationMillis,
@ -126,6 +129,7 @@ class AvesEntry {
sourceRotationDegrees: map['sourceRotationDegrees'] as int? ?? 0, sourceRotationDegrees: map['sourceRotationDegrees'] as int? ?? 0,
sizeBytes: map['sizeBytes'] as int?, sizeBytes: map['sizeBytes'] as int?,
sourceTitle: map['title'] as String?, sourceTitle: map['title'] as String?,
dateAddedSecs: map['dateAddedSecs'] as int?,
dateModifiedSecs: map['dateModifiedSecs'] as int?, dateModifiedSecs: map['dateModifiedSecs'] as int?,
sourceDateTakenMillis: map['sourceDateTakenMillis'] as int?, sourceDateTakenMillis: map['sourceDateTakenMillis'] as int?,
durationMillis: map['durationMillis'] as int?, durationMillis: map['durationMillis'] as int?,
@ -153,6 +157,23 @@ class AvesEntry {
}; };
} }
Map<String, dynamic> 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() { void dispose() {
imageChangeNotifier.dispose(); imageChangeNotifier.dispose();
metadataChangeNotifier.dispose(); metadataChangeNotifier.dispose();
@ -464,7 +485,7 @@ class AvesEntry {
String? _bestTitle; String? _bestTitle;
String? get bestTitle { String? get bestTitle {
_bestTitle ??= _catalogMetadata?.xmpTitleDescription?.isNotEmpty == true ? _catalogMetadata!.xmpTitleDescription : (filenameWithoutExtension ?? sourceTitle); _bestTitle ??= _catalogMetadata?.xmpTitle?.isNotEmpty == true ? _catalogMetadata!.xmpTitle : (filenameWithoutExtension ?? sourceTitle);
return _bestTitle; return _bestTitle;
} }

View file

@ -11,6 +11,7 @@ import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/filters/path.dart'; import 'package:aves/model/filters/path.dart';
import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/query.dart';
import 'package:aves/model/filters/rating.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/tag.dart';
import 'package:aves/model/filters/trash.dart'; import 'package:aves/model/filters/trash.dart';
import 'package:aves/model/filters/type.dart'; import 'package:aves/model/filters/type.dart';
@ -68,6 +69,8 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
return QueryFilter.fromMap(jsonMap); return QueryFilter.fromMap(jsonMap);
case RatingFilter.type: case RatingFilter.type:
return RatingFilter.fromMap(jsonMap); return RatingFilter.fromMap(jsonMap);
case RecentlyAddedFilter.type:
return RecentlyAddedFilter.instance;
case TagFilter.type: case TagFilter.type:
return TagFilter.fromMap(jsonMap); return TagFilter.fromMap(jsonMap);
case TypeFilter.type: case TypeFilter.type:

View file

@ -0,0 +1,48 @@
import 'package:aves/model/filters/filters.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart';
class RecentlyAddedFilter extends CollectionFilter {
static const type = 'recently_added';
static final instance = RecentlyAddedFilter._private();
static late int nowSecs;
static void updateNow() {
nowSecs = DateTime.now().millisecondsSinceEpoch ~/ 1000;
}
static const _dayInSecs = 24 * 60 * 60;
@override
List<Object?> get props => [];
RecentlyAddedFilter._private() {
updateNow();
}
@override
Map<String, dynamic> 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;
}

View file

@ -7,7 +7,7 @@ class CatalogMetadata {
final bool isAnimated, isGeotiff, is360, isMultiPage, isMotionPhoto; final bool isAnimated, isGeotiff, is360, isMultiPage, isMotionPhoto;
bool isFlipped; bool isFlipped;
int? rotationDegrees; int? rotationDegrees;
final String? mimeType, xmpSubjects, xmpTitleDescription; final String? mimeType, xmpSubjects, xmpTitle;
double? latitude, longitude; double? latitude, longitude;
Address? address; Address? address;
int rating; int rating;
@ -32,7 +32,7 @@ class CatalogMetadata {
this.isMotionPhoto = false, this.isMotionPhoto = false,
this.rotationDegrees, this.rotationDegrees,
this.xmpSubjects, this.xmpSubjects,
this.xmpTitleDescription, this.xmpTitle,
double? latitude, double? latitude,
double? longitude, double? longitude,
this.rating = 0, this.rating = 0,
@ -72,7 +72,7 @@ class CatalogMetadata {
isMotionPhoto: isMotionPhoto, isMotionPhoto: isMotionPhoto,
rotationDegrees: rotationDegrees ?? this.rotationDegrees, rotationDegrees: rotationDegrees ?? this.rotationDegrees,
xmpSubjects: xmpSubjects, xmpSubjects: xmpSubjects,
xmpTitleDescription: xmpTitleDescription, xmpTitle: xmpTitle,
latitude: latitude ?? this.latitude, latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude, longitude: longitude ?? this.longitude,
rating: rating, rating: rating,
@ -94,7 +94,7 @@ class CatalogMetadata {
// `rotationDegrees` should default to `sourceRotationDegrees`, not 0 // `rotationDegrees` should default to `sourceRotationDegrees`, not 0
rotationDegrees: map['rotationDegrees'], rotationDegrees: map['rotationDegrees'],
xmpSubjects: map['xmpSubjects'] ?? '', xmpSubjects: map['xmpSubjects'] ?? '',
xmpTitleDescription: map['xmpTitleDescription'] ?? '', xmpTitle: map['xmpTitle'] ?? '',
latitude: map['latitude'], latitude: map['latitude'],
longitude: map['longitude'], longitude: map['longitude'],
rating: map['rating'] ?? 0, 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), 'flags': (isAnimated ? _isAnimatedMask : 0) | (isFlipped ? _isFlippedMask : 0) | (isGeotiff ? _isGeotiffMask : 0) | (is360 ? _is360Mask : 0) | (isMultiPage ? _isMultiPageMask : 0) | (isMotionPhoto ? _isMotionPhotoMask : 0),
'rotationDegrees': rotationDegrees, 'rotationDegrees': rotationDegrees,
'xmpSubjects': xmpSubjects, 'xmpSubjects': xmpSubjects,
'xmpTitleDescription': xmpTitleDescription, 'xmpTitle': xmpTitle,
'latitude': latitude, 'latitude': latitude,
'longitude': longitude, 'longitude': longitude,
'rating': rating, 'rating': rating,
}; };
@override @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}';
} }

View file

@ -102,6 +102,7 @@ class MultiPageInfo {
sourceRotationDegrees: pageInfo.rotationDegrees ?? mainEntry.sourceRotationDegrees, sourceRotationDegrees: pageInfo.rotationDegrees ?? mainEntry.sourceRotationDegrees,
sizeBytes: mainEntry.sizeBytes, sizeBytes: mainEntry.sizeBytes,
sourceTitle: mainEntry.sourceTitle, sourceTitle: mainEntry.sourceTitle,
dateAddedSecs: mainEntry.dateAddedSecs,
dateModifiedSecs: mainEntry.dateModifiedSecs, dateModifiedSecs: mainEntry.dateModifiedSecs,
sourceDateTakenMillis: mainEntry.sourceDateTakenMillis, sourceDateTakenMillis: mainEntry.sourceDateTakenMillis,
durationMillis: pageInfo.durationMillis ?? mainEntry.durationMillis, durationMillis: pageInfo.durationMillis ?? mainEntry.durationMillis,

View file

@ -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/actions/entry_set_actions.dart';
import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/mime.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/naming_pattern.dart';
import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
@ -41,6 +42,7 @@ class SettingsDefaults {
null, null,
MimeFilter.video, MimeFilter.video,
FavouriteFilter.instance, FavouriteFilter.instance,
RecentlyAddedFilter.instance,
]; ];
static const drawerPageBookmarks = [ static const drawerPageBookmarks = [
AlbumListPage.routeName, AlbumListPage.routeName,

View file

@ -294,6 +294,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
contentId: newFields['contentId'] as int?, contentId: newFields['contentId'] as int?,
// 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['title'] as String?, title: newFields['title'] as String?,
dateAddedSecs: metadataDb.timestampSecs,
dateModifiedSecs: newFields['dateModifiedSecs'] as int?, dateModifiedSecs: newFields['dateModifiedSecs'] as int?,
)); ));
} else { } else {

View file

@ -158,7 +158,14 @@ class MediaStoreSource extends CollectionSource {
// when discovering modified entry with known content ID, // when discovering modified entry with known content ID,
// reuse known entry ID to overwrite it while preserving favourites, etc. // reuse known entry ID to overwrite it while preserving favourites, etc.
final contentId = entry.contentId; 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); pendingNewEntries.add(entry);
if (pendingNewEntries.length >= refreshCount) { if (pendingNewEntries.length >= refreshCount) {
@ -243,7 +250,13 @@ class MediaStoreSource extends CollectionSource {
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) {
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); newEntries.add(sourceEntry);
final existingDirectory = existingEntry?.directory; final existingDirectory = existingEntry?.directory;
if (existingDirectory != null) { if (existingDirectory != null) {

View file

@ -52,23 +52,6 @@ class PlatformMediaEditService implements MediaEditService {
static const _platform = MethodChannel('deckers.thibault/aves/media_edit'); static const _platform = MethodChannel('deckers.thibault/aves/media_edit');
static final _opStream = StreamsChannel('deckers.thibault/aves/media_op_stream'); static final _opStream = StreamsChannel('deckers.thibault/aves/media_op_stream');
static Map<String, dynamic> _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 @override
String get newOpId => DateTime.now().millisecondsSinceEpoch.toString(); String get newOpId => DateTime.now().millisecondsSinceEpoch.toString();
@ -93,7 +76,7 @@ class PlatformMediaEditService implements MediaEditService {
.receiveBroadcastStream(<String, dynamic>{ .receiveBroadcastStream(<String, dynamic>{
'op': 'delete', 'op': 'delete',
'id': opId, 'id': opId,
'entries': entries.map(_toPlatformEntryMap).toList(), 'entries': entries.map((entry) => entry.toPlatformEntryMap()).toList(),
}) })
.where((event) => event is Map) .where((event) => event is Map)
.map((event) => ImageOpEvent.fromMap(event as Map)); .map((event) => ImageOpEvent.fromMap(event as Map));
@ -115,7 +98,7 @@ class PlatformMediaEditService implements MediaEditService {
.receiveBroadcastStream(<String, dynamic>{ .receiveBroadcastStream(<String, dynamic>{
'op': 'move', 'op': 'move',
'id': opId, '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, 'copy': copy,
'nameConflictStrategy': nameConflictStrategy.toPlatform(), 'nameConflictStrategy': nameConflictStrategy.toPlatform(),
}) })
@ -138,7 +121,7 @@ class PlatformMediaEditService implements MediaEditService {
return _opStream return _opStream
.receiveBroadcastStream(<String, dynamic>{ .receiveBroadcastStream(<String, dynamic>{
'op': 'export', 'op': 'export',
'entries': entries.map(_toPlatformEntryMap).toList(), 'entries': entries.map((entry) => entry.toPlatformEntryMap()).toList(),
'mimeType': options.mimeType, 'mimeType': options.mimeType,
'width': options.width, 'width': options.width,
'height': options.height, 'height': options.height,
@ -163,7 +146,7 @@ class PlatformMediaEditService implements MediaEditService {
.receiveBroadcastStream(<String, dynamic>{ .receiveBroadcastStream(<String, dynamic>{
'op': 'rename', 'op': 'rename',
'id': opId, '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) .where((event) => event is Map)
.map((event) => MoveOpEvent.fromMap(event as Map)); .map((event) => MoveOpEvent.fromMap(event as Map));

View file

@ -25,27 +25,12 @@ abstract class MetadataEditService {
class PlatformMetadataEditService implements MetadataEditService { class PlatformMetadataEditService implements MetadataEditService {
static const _platform = MethodChannel('deckers.thibault/aves/metadata_edit'); static const _platform = MethodChannel('deckers.thibault/aves/metadata_edit');
static Map<String, dynamic> _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 @override
Future<Map<String, dynamic>> rotate(AvesEntry entry, {required bool clockwise}) async { Future<Map<String, dynamic>> rotate(AvesEntry entry, {required bool clockwise}) async {
try { try {
// returns map with: 'rotationDegrees' 'isFlipped' // returns map with: 'rotationDegrees' 'isFlipped'
final result = await _platform.invokeMethod('rotate', <String, dynamic>{ final result = await _platform.invokeMethod('rotate', <String, dynamic>{
'entry': _toPlatformEntryMap(entry), 'entry': entry.toPlatformEntryMap(),
'clockwise': clockwise, 'clockwise': clockwise,
}); });
if (result != null) return (result as Map).cast<String, dynamic>(); if (result != null) return (result as Map).cast<String, dynamic>();
@ -62,7 +47,7 @@ class PlatformMetadataEditService implements MetadataEditService {
try { try {
// returns map with: 'rotationDegrees' 'isFlipped' // returns map with: 'rotationDegrees' 'isFlipped'
final result = await _platform.invokeMethod('flip', <String, dynamic>{ final result = await _platform.invokeMethod('flip', <String, dynamic>{
'entry': _toPlatformEntryMap(entry), 'entry': entry.toPlatformEntryMap(),
}); });
if (result != null) return (result as Map).cast<String, dynamic>(); if (result != null) return (result as Map).cast<String, dynamic>();
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
@ -77,7 +62,7 @@ class PlatformMetadataEditService implements MetadataEditService {
Future<Map<String, dynamic>> editExifDate(AvesEntry entry, DateModifier modifier) async { Future<Map<String, dynamic>> editExifDate(AvesEntry entry, DateModifier modifier) async {
try { try {
final result = await _platform.invokeMethod('editDate', <String, dynamic>{ final result = await _platform.invokeMethod('editDate', <String, dynamic>{
'entry': _toPlatformEntryMap(entry), 'entry': entry.toPlatformEntryMap(),
'dateMillis': modifier.setDateTime?.millisecondsSinceEpoch, 'dateMillis': modifier.setDateTime?.millisecondsSinceEpoch,
'shiftMinutes': modifier.shiftMinutes, 'shiftMinutes': modifier.shiftMinutes,
'fields': modifier.fields.where((v) => v.type == MetadataType.exif).map((v) => v.exifInterfaceTag).whereNotNull().toList(), 'fields': modifier.fields.where((v) => v.type == MetadataType.exif).map((v) => v.exifInterfaceTag).whereNotNull().toList(),
@ -99,7 +84,7 @@ class PlatformMetadataEditService implements MetadataEditService {
}) async { }) async {
try { try {
final result = await _platform.invokeMethod('editMetadata', <String, dynamic>{ final result = await _platform.invokeMethod('editMetadata', <String, dynamic>{
'entry': _toPlatformEntryMap(entry), 'entry': entry.toPlatformEntryMap(),
'metadata': metadata.map((type, value) => MapEntry(_toPlatformMetadataType(type), value)), 'metadata': metadata.map((type, value) => MapEntry(_toPlatformMetadataType(type), value)),
'autoCorrectTrailerOffset': autoCorrectTrailerOffset, 'autoCorrectTrailerOffset': autoCorrectTrailerOffset,
}); });
@ -116,7 +101,7 @@ class PlatformMetadataEditService implements MetadataEditService {
Future<Map<String, dynamic>> removeTrailerVideo(AvesEntry entry) async { Future<Map<String, dynamic>> removeTrailerVideo(AvesEntry entry) async {
try { try {
final result = await _platform.invokeMethod('removeTrailerVideo', <String, dynamic>{ final result = await _platform.invokeMethod('removeTrailerVideo', <String, dynamic>{
'entry': _toPlatformEntryMap(entry), 'entry': entry.toPlatformEntryMap(),
}); });
if (result != null) return (result as Map).cast<String, dynamic>(); if (result != null) return (result as Map).cast<String, dynamic>();
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
@ -131,7 +116,7 @@ class PlatformMetadataEditService implements MetadataEditService {
Future<Map<String, dynamic>> removeTypes(AvesEntry entry, Set<MetadataType> types) async { Future<Map<String, dynamic>> removeTypes(AvesEntry entry, Set<MetadataType> types) async {
try { try {
final result = await _platform.invokeMethod('removeTypes', <String, dynamic>{ final result = await _platform.invokeMethod('removeTypes', <String, dynamic>{
'entry': _toPlatformEntryMap(entry), 'entry': entry.toPlatformEntryMap(),
'types': types.map(_toPlatformMetadataType).toList(), 'types': types.map(_toPlatformMetadataType).toList(),
}); });
if (result != null) return (result as Map).cast<String, dynamic>(); if (result != null) return (result as Map).cast<String, dynamic>();

View file

@ -77,7 +77,7 @@ class PlatformMetadataFetchService implements MetadataFetchService {
// 'latitude': latitude (double) // 'latitude': latitude (double)
// 'longitude': longitude (double) // 'longitude': longitude (double)
// 'xmpSubjects': ';' separated XMP subjects (string) // 'xmpSubjects': ';' separated XMP subjects (string)
// 'xmpTitleDescription': XMP title (string) // 'xmpTitle': XMP title (string)
final result = await _platform.invokeMethod('getCatalogMetadata', <String, dynamic>{ final result = await _platform.invokeMethod('getCatalogMetadata', <String, dynamic>{
'mimeType': entry.mimeType, 'mimeType': entry.mimeType,
'uri': entry.uri, 'uri': entry.uri,

View file

@ -35,6 +35,7 @@ class AIcons {
static const IconData ratingRejected = MdiIcons.starMinusOutline; static const IconData ratingRejected = MdiIcons.starMinusOutline;
static const IconData ratingUnrated = MdiIcons.starOffOutline; static const IconData ratingUnrated = MdiIcons.starOffOutline;
static const IconData raw = Icons.raw_on_outlined; 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 shooting = Icons.camera_outlined;
static const IconData removableStorage = Icons.sd_storage_outlined; static const IconData removableStorage = Icons.sd_storage_outlined;
static const IconData sensorControlEnabled = Icons.explore_outlined; static const IconData sensorControlEnabled = Icons.explore_outlined;

View file

@ -5,6 +5,7 @@ import 'package:aves/app_flavor.dart';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/l10n/l10n.dart'; import 'package:aves/l10n/l10n.dart';
import 'package:aves/model/device.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/defaults.dart';
import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/enums/display_refresh_rate_mode.dart'; import 'package:aves/model/settings/enums/display_refresh_rate_mode.dart';
@ -275,9 +276,11 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
break; break;
} }
break; break;
case AppLifecycleState.resumed:
RecentlyAddedFilter.updateNow();
break;
case AppLifecycleState.paused: case AppLifecycleState.paused:
case AppLifecycleState.detached: case AppLifecycleState.detached:
case AppLifecycleState.resumed:
break; break;
} }
} }

View file

@ -6,6 +6,7 @@ import 'package:aves/model/filters/location.dart';
import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/query.dart';
import 'package:aves/model/filters/rating.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/tag.dart';
import 'package:aves/model/filters/type.dart'; import 'package:aves/model/filters/type.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
@ -130,6 +131,7 @@ class CollectionSearchDelegate extends AvesSearchDelegate {
Widget _buildDateFilters(BuildContext context, _ContainQuery containQuery) { Widget _buildDateFilters(BuildContext context, _ContainQuery containQuery) {
final filters = [ final filters = [
DateFilter.onThisDay, DateFilter.onThisDay,
RecentlyAddedFilter.instance,
..._monthFilters, ..._monthFilters,
].where((f) => containQuery(f.getLabel(context))).toList(); ].where((f) => containQuery(f.getLabel(context))).toList();
return _buildFilterRow( return _buildFilterRow(

View file

@ -1,4 +1,5 @@
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/recent.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart';
@ -31,6 +32,7 @@ class _NavigationDrawerEditorPageState extends State<NavigationDrawerEditorPage>
static final Set<CollectionFilter?> _typeOptions = { static final Set<CollectionFilter?> _typeOptions = {
null, null,
RecentlyAddedFilter.instance,
...CollectionSearchDelegate.typeFilters, ...CollectionSearchDelegate.typeFilters,
}; };
static const Set<String> _pageOptions = { static const Set<String> _pageOptions = {

View file

@ -94,6 +94,7 @@ class _DbTabState extends State<DbTab> {
'sourceRotationDegrees': '${data.sourceRotationDegrees}', 'sourceRotationDegrees': '${data.sourceRotationDegrees}',
'sizeBytes': '${data.sizeBytes}', 'sizeBytes': '${data.sizeBytes}',
'sourceTitle': data.sourceTitle ?? '', 'sourceTitle': data.sourceTitle ?? '',
'dateAddedSecs': '${data.dateAddedSecs}',
'dateModifiedSecs': '${data.dateModifiedSecs}', 'dateModifiedSecs': '${data.dateModifiedSecs}',
'sourceDateTakenMillis': '${data.sourceDateTakenMillis}', 'sourceDateTakenMillis': '${data.sourceDateTakenMillis}',
'durationMillis': '${data.durationMillis}', 'durationMillis': '${data.durationMillis}',
@ -126,7 +127,7 @@ class _DbTabState extends State<DbTab> {
'latitude': '${data.latitude}', 'latitude': '${data.latitude}',
'longitude': '${data.longitude}', 'longitude': '${data.longitude}',
'xmpSubjects': data.xmpSubjects ?? '', 'xmpSubjects': data.xmpSubjects ?? '',
'xmpTitleDescription': data.xmpTitleDescription ?? '', 'xmpTitle': data.xmpTitle ?? '',
'rating': '${data.rating}', 'rating': '${data.rating}',
}, },
), ),

View file

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

View file

@ -38,6 +38,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
sourceRotationDegrees: 0, sourceRotationDegrees: 0,
sizeBytes: 42, sizeBytes: 42,
sourceTitle: filenameWithoutExtension, sourceTitle: filenameWithoutExtension,
dateAddedSecs: date,
dateModifiedSecs: date, dateModifiedSecs: date,
sourceDateTakenMillis: date, sourceDateTakenMillis: date,
durationMillis: null, durationMillis: null,

View file

@ -8,6 +8,7 @@ import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/filters/path.dart'; import 'package:aves/model/filters/path.dart';
import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/query.dart';
import 'package:aves/model/filters/rating.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/tag.dart';
import 'package:aves/model/filters/type.dart'; import 'package:aves/model/filters/type.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
@ -61,6 +62,9 @@ void main() {
const rating = RatingFilter(3); const rating = RatingFilter(3);
expect(rating, jsonRoundTrip(rating)); expect(rating, jsonRoundTrip(rating));
final recent = RecentlyAddedFilter.instance;
expect(recent, jsonRoundTrip(recent));
final tag = TagFilter('some tag'); final tag = TagFilter('some tag');
expect(tag, jsonRoundTrip(tag)); expect(tag, jsonRoundTrip(tag));

View file

@ -1,6 +1,7 @@
{ {
"de": [ "de": [
"entryInfoActionEditDescription", "entryInfoActionEditDescription",
"filterRecentlyAddedLabel",
"editEntryDescriptionDialogTitle", "editEntryDescriptionDialogTitle",
"settingsConfirmationAfterMoveToBinItems", "settingsConfirmationAfterMoveToBinItems",
"settingsViewerGestureSideTapNext" "settingsViewerGestureSideTapNext"
@ -8,18 +9,21 @@
"es": [ "es": [
"entryInfoActionEditDescription", "entryInfoActionEditDescription",
"filterRecentlyAddedLabel",
"editEntryDescriptionDialogTitle", "editEntryDescriptionDialogTitle",
"settingsConfirmationAfterMoveToBinItems" "settingsConfirmationAfterMoveToBinItems"
], ],
"fr": [ "fr": [
"entryInfoActionEditDescription", "entryInfoActionEditDescription",
"filterRecentlyAddedLabel",
"editEntryDescriptionDialogTitle", "editEntryDescriptionDialogTitle",
"settingsViewerGestureSideTapNext" "settingsViewerGestureSideTapNext"
], ],
"id": [ "id": [
"entryInfoActionEditDescription", "entryInfoActionEditDescription",
"filterRecentlyAddedLabel",
"editEntryDescriptionDialogTitle", "editEntryDescriptionDialogTitle",
"settingsConfirmationAfterMoveToBinItems", "settingsConfirmationAfterMoveToBinItems",
"settingsViewerGestureSideTapNext" "settingsViewerGestureSideTapNext"
@ -27,6 +31,7 @@
"it": [ "it": [
"entryInfoActionEditDescription", "entryInfoActionEditDescription",
"filterRecentlyAddedLabel",
"editEntryDescriptionDialogTitle", "editEntryDescriptionDialogTitle",
"settingsConfirmationAfterMoveToBinItems", "settingsConfirmationAfterMoveToBinItems",
"settingsViewerGestureSideTapNext" "settingsViewerGestureSideTapNext"
@ -34,6 +39,7 @@
"ja": [ "ja": [
"entryInfoActionEditDescription", "entryInfoActionEditDescription",
"filterRecentlyAddedLabel",
"editEntryDescriptionDialogTitle", "editEntryDescriptionDialogTitle",
"settingsConfirmationAfterMoveToBinItems", "settingsConfirmationAfterMoveToBinItems",
"settingsViewerGestureSideTapNext" "settingsViewerGestureSideTapNext"
@ -41,12 +47,14 @@
"ko": [ "ko": [
"entryInfoActionEditDescription", "entryInfoActionEditDescription",
"filterRecentlyAddedLabel",
"editEntryDescriptionDialogTitle", "editEntryDescriptionDialogTitle",
"settingsViewerGestureSideTapNext" "settingsViewerGestureSideTapNext"
], ],
"nl": [ "nl": [
"entryInfoActionEditDescription", "entryInfoActionEditDescription",
"filterRecentlyAddedLabel",
"editEntryDescriptionDialogTitle", "editEntryDescriptionDialogTitle",
"settingsConfirmationAfterMoveToBinItems", "settingsConfirmationAfterMoveToBinItems",
"settingsViewerGestureSideTapNext", "settingsViewerGestureSideTapNext",
@ -55,6 +63,7 @@
"pt": [ "pt": [
"entryInfoActionEditDescription", "entryInfoActionEditDescription",
"filterRecentlyAddedLabel",
"editEntryDescriptionDialogTitle", "editEntryDescriptionDialogTitle",
"settingsConfirmationAfterMoveToBinItems", "settingsConfirmationAfterMoveToBinItems",
"settingsViewerGestureSideTapNext" "settingsViewerGestureSideTapNext"
@ -63,6 +72,7 @@
"ru": [ "ru": [
"entryInfoActionEditDescription", "entryInfoActionEditDescription",
"filterOnThisDayLabel", "filterOnThisDayLabel",
"filterRecentlyAddedLabel",
"editEntryDescriptionDialogTitle", "editEntryDescriptionDialogTitle",
"settingsConfirmationAfterMoveToBinItems", "settingsConfirmationAfterMoveToBinItems",
"settingsViewerGestureSideTapNext", "settingsViewerGestureSideTapNext",
@ -77,6 +87,7 @@
"slideshowActionShowInCollection", "slideshowActionShowInCollection",
"entryInfoActionEditDescription", "entryInfoActionEditDescription",
"filterOnThisDayLabel", "filterOnThisDayLabel",
"filterRecentlyAddedLabel",
"slideshowVideoPlaybackSkip", "slideshowVideoPlaybackSkip",
"slideshowVideoPlaybackMuted", "slideshowVideoPlaybackMuted",
"slideshowVideoPlaybackWithSound", "slideshowVideoPlaybackWithSound",
@ -110,6 +121,7 @@
"zh": [ "zh": [
"entryInfoActionEditDescription", "entryInfoActionEditDescription",
"filterRecentlyAddedLabel",
"editEntryDescriptionDialogTitle", "editEntryDescriptionDialogTitle",
"settingsConfirmationAfterMoveToBinItems", "settingsConfirmationAfterMoveToBinItems",
"settingsViewerGestureSideTapNext" "settingsViewerGestureSideTapNext"