#97 search: recently added filter
This commit is contained in:
parent
0cce0c1e11
commit
503f93ed17
26 changed files with 206 additions and 58 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -125,6 +125,7 @@
|
|||
"filterLocationEmptyLabel": "Unlocated",
|
||||
"filterTagEmptyLabel": "Untagged",
|
||||
"filterOnThisDayLabel": "On this day",
|
||||
"filterRecentlyAddedLabel": "Recently added",
|
||||
"filterRatingUnratedLabel": "Unrated",
|
||||
"filterRatingRejectedLabel": "Rejected",
|
||||
"filterTypeAnimatedLabel": "Animated",
|
||||
|
|
|
@ -10,6 +10,8 @@ import 'package:aves/model/video_playback.dart';
|
|||
abstract class MetadataDb {
|
||||
int get nextId;
|
||||
|
||||
int get timestampSecs;
|
||||
|
||||
Future<void> init();
|
||||
|
||||
Future<int> dbFileSize();
|
||||
|
|
|
@ -34,6 +34,9 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
@override
|
||||
int get nextId => ++_lastId;
|
||||
|
||||
@override
|
||||
int get timestampSecs => DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
@override
|
||||
Future<void> 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');
|
||||
|
|
|
@ -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<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;');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<AvesEntry>? 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<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() {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Collecti
|
|||
return QueryFilter.fromMap(jsonMap);
|
||||
case RatingFilter.type:
|
||||
return RatingFilter.fromMap(jsonMap);
|
||||
case RecentlyAddedFilter.type:
|
||||
return RecentlyAddedFilter.instance;
|
||||
case TagFilter.type:
|
||||
return TagFilter.fromMap(jsonMap);
|
||||
case TypeFilter.type:
|
||||
|
|
48
lib/model/filters/recent.dart
Normal file
48
lib/model/filters/recent.dart
Normal 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;
|
||||
}
|
|
@ -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}';
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<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
|
||||
String get newOpId => DateTime.now().millisecondsSinceEpoch.toString();
|
||||
|
||||
|
@ -93,7 +76,7 @@ class PlatformMediaEditService implements MediaEditService {
|
|||
.receiveBroadcastStream(<String, dynamic>{
|
||||
'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(<String, dynamic>{
|
||||
'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(<String, dynamic>{
|
||||
'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(<String, dynamic>{
|
||||
'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));
|
||||
|
|
|
@ -25,27 +25,12 @@ abstract class MetadataEditService {
|
|||
class PlatformMetadataEditService implements MetadataEditService {
|
||||
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
|
||||
Future<Map<String, dynamic>> rotate(AvesEntry entry, {required bool clockwise}) async {
|
||||
try {
|
||||
// returns map with: 'rotationDegrees' 'isFlipped'
|
||||
final result = await _platform.invokeMethod('rotate', <String, dynamic>{
|
||||
'entry': _toPlatformEntryMap(entry),
|
||||
'entry': entry.toPlatformEntryMap(),
|
||||
'clockwise': clockwise,
|
||||
});
|
||||
if (result != null) return (result as Map).cast<String, dynamic>();
|
||||
|
@ -62,7 +47,7 @@ class PlatformMetadataEditService implements MetadataEditService {
|
|||
try {
|
||||
// returns map with: 'rotationDegrees' 'isFlipped'
|
||||
final result = await _platform.invokeMethod('flip', <String, dynamic>{
|
||||
'entry': _toPlatformEntryMap(entry),
|
||||
'entry': entry.toPlatformEntryMap(),
|
||||
});
|
||||
if (result != null) return (result as Map).cast<String, dynamic>();
|
||||
} on PlatformException catch (e, stack) {
|
||||
|
@ -77,7 +62,7 @@ class PlatformMetadataEditService implements MetadataEditService {
|
|||
Future<Map<String, dynamic>> editExifDate(AvesEntry entry, DateModifier modifier) async {
|
||||
try {
|
||||
final result = await _platform.invokeMethod('editDate', <String, dynamic>{
|
||||
'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', <String, dynamic>{
|
||||
'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<Map<String, dynamic>> removeTrailerVideo(AvesEntry entry) async {
|
||||
try {
|
||||
final result = await _platform.invokeMethod('removeTrailerVideo', <String, dynamic>{
|
||||
'entry': _toPlatformEntryMap(entry),
|
||||
'entry': entry.toPlatformEntryMap(),
|
||||
});
|
||||
if (result != null) return (result as Map).cast<String, dynamic>();
|
||||
} on PlatformException catch (e, stack) {
|
||||
|
@ -131,7 +116,7 @@ class PlatformMetadataEditService implements MetadataEditService {
|
|||
Future<Map<String, dynamic>> removeTypes(AvesEntry entry, Set<MetadataType> types) async {
|
||||
try {
|
||||
final result = await _platform.invokeMethod('removeTypes', <String, dynamic>{
|
||||
'entry': _toPlatformEntryMap(entry),
|
||||
'entry': entry.toPlatformEntryMap(),
|
||||
'types': types.map(_toPlatformMetadataType).toList(),
|
||||
});
|
||||
if (result != null) return (result as Map).cast<String, dynamic>();
|
||||
|
|
|
@ -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', <String, dynamic>{
|
||||
'mimeType': entry.mimeType,
|
||||
'uri': entry.uri,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<AvesApp> with WidgetsBindingObserver {
|
|||
break;
|
||||
}
|
||||
break;
|
||||
case AppLifecycleState.resumed:
|
||||
RecentlyAddedFilter.updateNow();
|
||||
break;
|
||||
case AppLifecycleState.paused:
|
||||
case AppLifecycleState.detached:
|
||||
case AppLifecycleState.resumed:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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<NavigationDrawerEditorPage>
|
|||
|
||||
static final Set<CollectionFilter?> _typeOptions = {
|
||||
null,
|
||||
RecentlyAddedFilter.instance,
|
||||
...CollectionSearchDelegate.typeFilters,
|
||||
};
|
||||
static const Set<String> _pageOptions = {
|
||||
|
|
|
@ -94,6 +94,7 @@ class _DbTabState extends State<DbTab> {
|
|||
'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<DbTab> {
|
|||
'latitude': '${data.latitude}',
|
||||
'longitude': '${data.longitude}',
|
||||
'xmpSubjects': data.xmpSubjects ?? '',
|
||||
'xmpTitleDescription': data.xmpTitleDescription ?? '',
|
||||
'xmpTitle': data.xmpTitle ?? '',
|
||||
'rating': '${data.rating}',
|
||||
},
|
||||
),
|
||||
|
|
|
@ -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}',
|
||||
|
|
|
@ -38,6 +38,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
|
|||
sourceRotationDegrees: 0,
|
||||
sizeBytes: 42,
|
||||
sourceTitle: filenameWithoutExtension,
|
||||
dateAddedSecs: date,
|
||||
dateModifiedSecs: date,
|
||||
sourceDateTakenMillis: date,
|
||||
durationMillis: null,
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue