diff --git a/lib/model/db/db_sqflite.dart b/lib/model/db/db_sqflite.dart index 91da588e6..7907f588f 100644 --- a/lib/model/db/db_sqflite.dart +++ b/lib/model/db/db_sqflite.dart @@ -179,7 +179,7 @@ class SqfliteLocalMediaDb implements LocalMediaDb { void _batchInsertEntry(Batch batch, AvesEntry entry) { batch.insert( entryTable, - entry.toMap(), + entry.toDatabaseMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); } diff --git a/lib/model/entry/entry.dart b/lib/model/entry/entry.dart index 763e43390..b6f7cbcd2 100644 --- a/lib/model/entry/entry.dart +++ b/lib/model/entry/entry.dart @@ -149,7 +149,7 @@ class AvesEntry with AvesEntryBase { } // for DB only - Map toMap() { + Map toDatabaseMap() { return { EntryFields.id: id, EntryFields.uri: uri, @@ -397,6 +397,17 @@ class AvesEntry with AvesEntryBase { }.nonNulls.where((v) => v.isNotEmpty).join(', '); } + static void normalizeMimeTypeFields(Map fields) { + final mimeType = fields[EntryFields.mimeType] as String?; + if (mimeType != null) { + fields[EntryFields.mimeType] = MimeTypes.normalize(mimeType); + } + final sourceMimeType = fields[EntryFields.sourceMimeType] as String?; + if (sourceMimeType != null) { + fields[EntryFields.sourceMimeType] = MimeTypes.normalize(sourceMimeType); + } + } + Future applyNewFields(Map newFields, {required bool persist}) async { final oldMimeType = mimeType; final oldDateModifiedMillis = this.dateModifiedMillis; @@ -458,7 +469,7 @@ class AvesEntry with AvesEntryBase { final updatedEntry = await mediaFetchService.getEntry(uri, mimeType); if (updatedEntry != null) { - await applyNewFields(updatedEntry.toMap(), persist: persist); + await applyNewFields(updatedEntry.toDatabaseMap(), persist: persist); } } diff --git a/lib/model/media/video/metadata.dart b/lib/model/media/video/metadata.dart index 212deb406..46a885cf9 100644 --- a/lib/model/media/video/metadata.dart +++ b/lib/model/media/video/metadata.dart @@ -114,9 +114,7 @@ class VideoMetadataFormatter { // exclude date if it is suspiciously close to epoch if (dateMillis != null && !DateTime.fromMillisecondsSinceEpoch(dateMillis).isAtSameDayAs(epoch)) { - catalogMetadata = catalogMetadata.copyWith( - dateMillis: dateMillis, - ); + catalogMetadata = catalogMetadata.copyWith(dateMillis: dateMillis); } return catalogMetadata; diff --git a/lib/model/multipage.dart b/lib/model/multipage.dart index af8a0d724..9755955c7 100644 --- a/lib/model/multipage.dart +++ b/lib/model/multipage.dart @@ -1,4 +1,5 @@ import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/extensions/keys.dart'; import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/services.dart'; @@ -81,17 +82,17 @@ class MultiPageInfo { final videoPage = _pages.firstWhereOrNull((page) => page.isVideo); if (videoPage != null && videoPage.uri == null) { final fields = await embeddedDataService.extractMotionPhotoVideo(mainEntry); - if (fields.containsKey('uri')) { + if (fields.containsKey(EntryFields.uri)) { final pageIndex = _pages.indexOf(videoPage); _pages.removeAt(pageIndex); _pages.insert( pageIndex, videoPage.copyWith( - uri: fields['uri'] as String?, + uri: fields[EntryFields.uri] as String?, // the initial fake page may contain inaccurate values for the following fields // so we override them with values from the extracted standalone video - rotationDegrees: fields['sourceRotationDegrees'] as int?, - durationMillis: fields['durationMillis'] as int?, + rotationDegrees: fields[EntryFields.sourceRotationDegrees] as int?, + durationMillis: fields[EntryFields.durationMillis] as int?, )); _pageEntries.remove(videoPage); } diff --git a/lib/ref/mime_types.dart b/lib/ref/mime_types.dart index 7ff064ea6..4dd62d597 100644 --- a/lib/ref/mime_types.dart +++ b/lib/ref/mime_types.dart @@ -100,7 +100,7 @@ class MimeTypes { static bool isVisual(String mimeType) => isImage(mimeType) || isVideo(mimeType); - static String _collapsedType(String mimeType) { + static String normalize(String mimeType) { switch (mimeType) { case avi: case aviMSVideo: @@ -127,7 +127,7 @@ class MimeTypes { } } - static bool refersToSameType(String a, b) => _collapsedType(a) == _collapsedType(b); + static bool refersToSameType(String a, b) => normalize(a) == normalize(b); static String? forExtension(String extension) { switch (extension) { diff --git a/lib/services/media/media_fetch_service.dart b/lib/services/media/media_fetch_service.dart index 438e2d07c..4d1bb10b0 100644 --- a/lib/services/media/media_fetch_service.dart +++ b/lib/services/media/media_fetch_service.dart @@ -81,6 +81,7 @@ class PlatformMediaFetchService implements MediaFetchService { 'mimeType': mimeType, 'allowUnsized': allowUnsized, }) as Map; + AvesEntry.normalizeMimeTypeFields(result); return AvesEntry.fromMap(result); } on PlatformException catch (e, stack) { // do not report issues with media content as it is likely an obsolete Media Store entry diff --git a/lib/services/media/media_store_service.dart b/lib/services/media/media_store_service.dart index c0e634867..394caf8f8 100644 --- a/lib/services/media/media_store_service.dart +++ b/lib/services/media/media_store_service.dart @@ -85,7 +85,11 @@ class PlatformMediaStoreService implements MediaStoreService { 'directory': directory, }) .where((event) => event is Map) - .map((event) => AvesEntry.fromMap(event as Map)); + .map((event) { + final fields = event as Map; + AvesEntry.normalizeMimeTypeFields(fields); + return AvesEntry.fromMap(fields); + }); } on PlatformException catch (e, stack) { reportService.recordError(e, stack); return Stream.error(e); diff --git a/lib/services/metadata/metadata_fetch_service.dart b/lib/services/metadata/metadata_fetch_service.dart index aa2283587..9f10284e6 100644 --- a/lib/services/metadata/metadata_fetch_service.dart +++ b/lib/services/metadata/metadata_fetch_service.dart @@ -3,10 +3,10 @@ import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/media/geotiff.dart'; +import 'package:aves/model/media/panorama.dart'; import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/model/metadata/overlay.dart'; import 'package:aves/model/multipage.dart'; -import 'package:aves/model/media/panorama.dart'; import 'package:aves/services/common/service_policy.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/metadata/xmp.dart'; @@ -88,6 +88,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { 'sizeBytes': entry.sizeBytes, }) as Map; result['id'] = entry.id; + AvesEntry.normalizeMimeTypeFields(result); return CatalogMetadata.fromMap(result); } on PlatformException catch (e, stack) { if (entry.isValid) { @@ -164,6 +165,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { imagePage['height'] = entry.height; imagePage['rotationDegrees'] = entry.rotationDegrees; } + pageMaps.forEach(AvesEntry.normalizeMimeTypeFields); return MultiPageInfo.fromPageMaps(entry, pageMaps); } on PlatformException catch (e, stack) { if (entry.isValid) { diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index a16922c1f..50bc64cc6 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:aves/app_mode.dart'; import 'package:aves/model/covers.dart'; import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/rating.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/settings.dart'; @@ -106,12 +107,18 @@ class AvesFilterChip extends StatefulWidget { const touchArea = Size(kMinInteractiveDimension, kMinInteractiveDimension); final actionDelegate = ChipActionDelegate(); final animations = context.read().accessibilityAnimations; + + var title = filter.getLabel(context); + if (filter is MimeFilter) { + title += ' (${filter.mime})'; + } + final selectedAction = await showMenu( context: context, position: RelativeRect.fromRect(tapPosition & touchArea, Offset.zero & overlay.size), items: [ PopupMenuItem( - child: Text(filter.getLabel(context)), + child: Text(title), ), const PopupMenuDivider(), ...ChipAction.values.where((action) => actionDelegate.isVisible(action, filter: filter)).map((action) { diff --git a/lib/widgets/viewer/debug/db.dart b/lib/widgets/viewer/debug/db.dart index 74447103c..c0dfe4c26 100644 --- a/lib/widgets/viewer/debug/db.dart +++ b/lib/widgets/viewer/debug/db.dart @@ -103,7 +103,7 @@ class _DbTabState extends State { child: const Text('Duplicate entry'), ), InfoRowGroup( - info: Map.fromEntries(data.toMap().entries.map((kv) => MapEntry(kv.key, kv.value?.toString() ?? ''))), + info: Map.fromEntries(data.toDatabaseMap().entries.map((kv) => MapEntry(kv.key, kv.value?.toString() ?? ''))), ), ], ], diff --git a/lib/widgets/viewer/info/embedded/embedded_data_opener.dart b/lib/widgets/viewer/info/embedded/embedded_data_opener.dart index 331743110..917fa6380 100644 --- a/lib/widgets/viewer/info/embedded/embedded_data_opener.dart +++ b/lib/widgets/viewer/info/embedded/embedded_data_opener.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/extensions/keys.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; @@ -51,13 +52,14 @@ class EmbeddedDataOpener extends StatelessWidget with FeedbackMixin { case EmbeddedDataSource.xmp: fields = await embeddedDataService.extractXmpDataProp(entry, notification.props, notification.mimeType); } - if (!fields.containsKey('mimeType') || !fields.containsKey('uri')) { + AvesEntry.normalizeMimeTypeFields(fields); + final mimeType = fields[EntryFields.mimeType] as String?; + final uri = fields[EntryFields.uri] as String?; + if (mimeType == null || uri == null) { showFeedback(context, FeedbackType.warn, context.l10n.viewerInfoOpenEmbeddedFailureFeedback); return; } - final mimeType = fields['mimeType']!; - final uri = fields['uri']!; if (!MimeTypes.isImage(mimeType) && !MimeTypes.isVideo(mimeType)) { // open with another app unawaited(appService.open(uri, mimeType, forceChooser: true).then((success) {