#1507 mime type normalization

This commit is contained in:
Thibault Deckers 2025-04-20 18:33:28 +02:00
parent e8eae7e9db
commit 4df4738dd3
11 changed files with 45 additions and 19 deletions

View file

@ -179,7 +179,7 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
void _batchInsertEntry(Batch batch, AvesEntry entry) { void _batchInsertEntry(Batch batch, AvesEntry entry) {
batch.insert( batch.insert(
entryTable, entryTable,
entry.toMap(), entry.toDatabaseMap(),
conflictAlgorithm: ConflictAlgorithm.replace, conflictAlgorithm: ConflictAlgorithm.replace,
); );
} }

View file

@ -149,7 +149,7 @@ class AvesEntry with AvesEntryBase {
} }
// for DB only // for DB only
Map<String, dynamic> toMap() { Map<String, dynamic> toDatabaseMap() {
return { return {
EntryFields.id: id, EntryFields.id: id,
EntryFields.uri: uri, EntryFields.uri: uri,
@ -397,6 +397,17 @@ class AvesEntry with AvesEntryBase {
}.nonNulls.where((v) => v.isNotEmpty).join(', '); }.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<void> applyNewFields(Map newFields, {required bool persist}) async { Future<void> applyNewFields(Map newFields, {required bool persist}) async {
final oldMimeType = mimeType; final oldMimeType = mimeType;
final oldDateModifiedMillis = this.dateModifiedMillis; final oldDateModifiedMillis = this.dateModifiedMillis;
@ -458,7 +469,7 @@ class AvesEntry with AvesEntryBase {
final updatedEntry = await mediaFetchService.getEntry(uri, mimeType); final updatedEntry = await mediaFetchService.getEntry(uri, mimeType);
if (updatedEntry != null) { if (updatedEntry != null) {
await applyNewFields(updatedEntry.toMap(), persist: persist); await applyNewFields(updatedEntry.toDatabaseMap(), persist: persist);
} }
} }

View file

@ -114,9 +114,7 @@ class VideoMetadataFormatter {
// exclude date if it is suspiciously close to epoch // exclude date if it is suspiciously close to epoch
if (dateMillis != null && !DateTime.fromMillisecondsSinceEpoch(dateMillis).isAtSameDayAs(epoch)) { if (dateMillis != null && !DateTime.fromMillisecondsSinceEpoch(dateMillis).isAtSameDayAs(epoch)) {
catalogMetadata = catalogMetadata.copyWith( catalogMetadata = catalogMetadata.copyWith(dateMillis: dateMillis);
dateMillis: dateMillis,
);
} }
return catalogMetadata; return catalogMetadata;

View file

@ -1,4 +1,5 @@
import 'package:aves/model/entry/entry.dart'; 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/model/entry/extensions/multipage.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
@ -81,17 +82,17 @@ class MultiPageInfo {
final videoPage = _pages.firstWhereOrNull((page) => page.isVideo); final videoPage = _pages.firstWhereOrNull((page) => page.isVideo);
if (videoPage != null && videoPage.uri == null) { if (videoPage != null && videoPage.uri == null) {
final fields = await embeddedDataService.extractMotionPhotoVideo(mainEntry); final fields = await embeddedDataService.extractMotionPhotoVideo(mainEntry);
if (fields.containsKey('uri')) { if (fields.containsKey(EntryFields.uri)) {
final pageIndex = _pages.indexOf(videoPage); final pageIndex = _pages.indexOf(videoPage);
_pages.removeAt(pageIndex); _pages.removeAt(pageIndex);
_pages.insert( _pages.insert(
pageIndex, pageIndex,
videoPage.copyWith( 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 // the initial fake page may contain inaccurate values for the following fields
// so we override them with values from the extracted standalone video // so we override them with values from the extracted standalone video
rotationDegrees: fields['sourceRotationDegrees'] as int?, rotationDegrees: fields[EntryFields.sourceRotationDegrees] as int?,
durationMillis: fields['durationMillis'] as int?, durationMillis: fields[EntryFields.durationMillis] as int?,
)); ));
_pageEntries.remove(videoPage); _pageEntries.remove(videoPage);
} }

View file

@ -100,7 +100,7 @@ class MimeTypes {
static bool isVisual(String mimeType) => isImage(mimeType) || isVideo(mimeType); static bool isVisual(String mimeType) => isImage(mimeType) || isVideo(mimeType);
static String _collapsedType(String mimeType) { static String normalize(String mimeType) {
switch (mimeType) { switch (mimeType) {
case avi: case avi:
case aviMSVideo: 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) { static String? forExtension(String extension) {
switch (extension) { switch (extension) {

View file

@ -81,6 +81,7 @@ class PlatformMediaFetchService implements MediaFetchService {
'mimeType': mimeType, 'mimeType': mimeType,
'allowUnsized': allowUnsized, 'allowUnsized': allowUnsized,
}) as Map; }) as Map;
AvesEntry.normalizeMimeTypeFields(result);
return AvesEntry.fromMap(result); return AvesEntry.fromMap(result);
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
// do not report issues with media content as it is likely an obsolete Media Store entry // do not report issues with media content as it is likely an obsolete Media Store entry

View file

@ -85,7 +85,11 @@ class PlatformMediaStoreService implements MediaStoreService {
'directory': directory, 'directory': directory,
}) })
.where((event) => event is Map) .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) { } on PlatformException catch (e, stack) {
reportService.recordError(e, stack); reportService.recordError(e, stack);
return Stream.error(e); return Stream.error(e);

View file

@ -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/multipage.dart';
import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/media/geotiff.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/catalog.dart';
import 'package:aves/model/metadata/overlay.dart'; import 'package:aves/model/metadata/overlay.dart';
import 'package:aves/model/multipage.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/service_policy.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/services/metadata/xmp.dart'; import 'package:aves/services/metadata/xmp.dart';
@ -88,6 +88,7 @@ class PlatformMetadataFetchService implements MetadataFetchService {
'sizeBytes': entry.sizeBytes, 'sizeBytes': entry.sizeBytes,
}) as Map; }) as Map;
result['id'] = entry.id; result['id'] = entry.id;
AvesEntry.normalizeMimeTypeFields(result);
return CatalogMetadata.fromMap(result); return CatalogMetadata.fromMap(result);
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
if (entry.isValid) { if (entry.isValid) {
@ -164,6 +165,7 @@ class PlatformMetadataFetchService implements MetadataFetchService {
imagePage['height'] = entry.height; imagePage['height'] = entry.height;
imagePage['rotationDegrees'] = entry.rotationDegrees; imagePage['rotationDegrees'] = entry.rotationDegrees;
} }
pageMaps.forEach(AvesEntry.normalizeMimeTypeFields);
return MultiPageInfo.fromPageMaps(entry, pageMaps); return MultiPageInfo.fromPageMaps(entry, pageMaps);
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
if (entry.isValid) { if (entry.isValid) {

View file

@ -4,6 +4,7 @@ import 'dart:math';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/covers.dart'; import 'package:aves/model/covers.dart';
import 'package:aves/model/filters/filters.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/filters/rating.dart';
import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
@ -106,12 +107,18 @@ class AvesFilterChip extends StatefulWidget {
const touchArea = Size(kMinInteractiveDimension, kMinInteractiveDimension); const touchArea = Size(kMinInteractiveDimension, kMinInteractiveDimension);
final actionDelegate = ChipActionDelegate(); final actionDelegate = ChipActionDelegate();
final animations = context.read<Settings>().accessibilityAnimations; final animations = context.read<Settings>().accessibilityAnimations;
var title = filter.getLabel(context);
if (filter is MimeFilter) {
title += ' (${filter.mime})';
}
final selectedAction = await showMenu<ChipAction>( final selectedAction = await showMenu<ChipAction>(
context: context, context: context,
position: RelativeRect.fromRect(tapPosition & touchArea, Offset.zero & overlay.size), position: RelativeRect.fromRect(tapPosition & touchArea, Offset.zero & overlay.size),
items: [ items: [
PopupMenuItem( PopupMenuItem(
child: Text(filter.getLabel(context)), child: Text(title),
), ),
const PopupMenuDivider(), const PopupMenuDivider(),
...ChipAction.values.where((action) => actionDelegate.isVisible(action, filter: filter)).map((action) { ...ChipAction.values.where((action) => actionDelegate.isVisible(action, filter: filter)).map((action) {

View file

@ -103,7 +103,7 @@ class _DbTabState extends State<DbTab> {
child: const Text('Duplicate entry'), child: const Text('Duplicate entry'),
), ),
InfoRowGroup( 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() ?? ''))),
), ),
], ],
], ],

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/entry/entry.dart'; 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/ref/mime_types.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart';
@ -51,13 +52,14 @@ class EmbeddedDataOpener extends StatelessWidget with FeedbackMixin {
case EmbeddedDataSource.xmp: case EmbeddedDataSource.xmp:
fields = await embeddedDataService.extractXmpDataProp(entry, notification.props, notification.mimeType); 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); showFeedback(context, FeedbackType.warn, context.l10n.viewerInfoOpenEmbeddedFailureFeedback);
return; return;
} }
final mimeType = fields['mimeType']!;
final uri = fields['uri']!;
if (!MimeTypes.isImage(mimeType) && !MimeTypes.isVideo(mimeType)) { if (!MimeTypes.isImage(mimeType) && !MimeTypes.isVideo(mimeType)) {
// open with another app // open with another app
unawaited(appService.open(uri, mimeType, forceChooser: true).then((success) { unawaited(appService.open(uri, mimeType, forceChooser: true).then((success) {