247 lines
9.6 KiB
Dart
247 lines
9.6 KiB
Dart
import 'package:aves/ref/brand_colors.dart';
|
|
import 'package:aves/ref/metadata/xmp.dart';
|
|
import 'package:aves/theme/colors.dart';
|
|
import 'package:aves/utils/string_utils.dart';
|
|
import 'package:aves/utils/xmp_utils.dart';
|
|
import 'package:aves/view/view.dart';
|
|
import 'package:aves/widgets/common/identity/highlight_title.dart';
|
|
import 'package:aves/widgets/viewer/info/common.dart';
|
|
import 'package:aves/widgets/viewer/info/metadata/xmp_card.dart';
|
|
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/crs.dart';
|
|
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/exif.dart';
|
|
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/google.dart';
|
|
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/misc.dart';
|
|
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/photoshop.dart';
|
|
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/tiff.dart';
|
|
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/xmp.dart';
|
|
import 'package:collection/collection.dart';
|
|
import 'package:equatable/equatable.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
@immutable
|
|
class XmpNamespace extends Equatable {
|
|
final Map<String, String> schemaRegistryPrefixes;
|
|
final String nsUri, nsPrefix;
|
|
final Map<String, String> rawProps;
|
|
|
|
@override
|
|
List<Object?> get props => [nsUri, nsPrefix];
|
|
|
|
XmpNamespace({
|
|
required this.nsUri,
|
|
required this.schemaRegistryPrefixes,
|
|
required this.rawProps,
|
|
}) : nsPrefix = prefixForUri(schemaRegistryPrefixes, nsUri);
|
|
|
|
factory XmpNamespace.create(Map<String, String> schemaRegistryPrefixes, String nsPrefix, Map<String, String> rawProps) {
|
|
final nsUri = schemaRegistryPrefixes[nsPrefix] ?? '';
|
|
switch (nsUri) {
|
|
case XmpNamespaces.creatorAtom:
|
|
return XmpCreatorAtom(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
case XmpNamespaces.crs:
|
|
return XmpCrsNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
case XmpNamespaces.darktable:
|
|
return XmpDarktableNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
case XmpNamespaces.dwc:
|
|
return XmpDwcNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
case XmpNamespaces.exif:
|
|
return XmpExifNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
case XmpNamespaces.gAudio:
|
|
return XmpGAudioNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
case XmpNamespaces.gCamera:
|
|
return XmpGCameraNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
case XmpNamespaces.gContainer:
|
|
return XmpGContainer(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
case XmpNamespaces.gDepth:
|
|
return XmpGDepthNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
case XmpNamespaces.gDevice:
|
|
return XmpGDeviceNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
case XmpNamespaces.gImage:
|
|
return XmpGImageNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
case XmpNamespaces.iptc4xmpCore:
|
|
return XmpIptcCoreNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
case XmpNamespaces.iptc4xmpExt:
|
|
return XmpIptc4xmpExtNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
case XmpNamespaces.mwgrs:
|
|
return XmpMgwRegionsNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
case XmpNamespaces.mp:
|
|
return XmpMPNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
case XmpNamespaces.photoshop:
|
|
return XmpPhotoshopNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
case XmpNamespaces.plus:
|
|
return XmpPlusNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
case XmpNamespaces.tiff:
|
|
return XmpTiffNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
case XmpNamespaces.xmp:
|
|
return XmpBasicNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
case XmpNamespaces.xmpMM:
|
|
return XmpMMNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
case XmpNamespaces.xperiaCamera:
|
|
return XmpXperiaCameraNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
default:
|
|
return XmpNamespace(nsUri: nsUri, schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
|
|
}
|
|
}
|
|
|
|
String get displayTitle => XmpNamespaceView.nsTitles[nsUri] ?? (nsPrefix.isEmpty ? nsUri : '${nsPrefix.substring(0, nsPrefix.length - 1)} ($nsUri)');
|
|
|
|
List<Widget> buildNamespaceSection(BuildContext context) {
|
|
final props = rawProps.entries
|
|
.map((kv) {
|
|
final key = kv.key;
|
|
if (skippedProps.any((pattern) => pattern.allMatches(key).isNotEmpty)) {
|
|
return null;
|
|
}
|
|
final prop = XmpProp(key, kv.value);
|
|
var extracted = false;
|
|
cards.forEach((card) => extracted |= card.extract(prop));
|
|
return extracted ? null : prop;
|
|
})
|
|
.nonNulls
|
|
.toList()
|
|
..sort();
|
|
|
|
final content = [
|
|
if (props.isNotEmpty)
|
|
InfoRowGroup(
|
|
info: Map.fromEntries(props.map((v) => MapEntry(v.displayKey, formatValue(v)))),
|
|
spanBuilders: linkifyValues(props),
|
|
),
|
|
...cards.where((v) => !v.isEmpty).map((card) {
|
|
final spanBuilders = card.spanBuilders;
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: 8),
|
|
child: XmpCard(
|
|
title: card.title,
|
|
structByIndex: card.data,
|
|
formatValue: formatValue,
|
|
spanBuilders: spanBuilders != null ? (index) => spanBuilders(index, card.data[index]!.$1) : null,
|
|
),
|
|
);
|
|
}),
|
|
];
|
|
|
|
return content.isNotEmpty
|
|
? [
|
|
if (displayTitle.isNotEmpty)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 8, bottom: 4),
|
|
child: HighlightTitle(
|
|
title: displayTitle,
|
|
color: context.select<AvesColorsData, Color?>((v) => v.fromBrandColor(BrandColors.get(displayTitle))),
|
|
),
|
|
),
|
|
...content
|
|
]
|
|
: [];
|
|
}
|
|
|
|
Set<RegExp> get skippedProps => {};
|
|
|
|
List<XmpCardData> get cards => [];
|
|
|
|
String formatValue(XmpProp prop) => prop.value;
|
|
|
|
Map<String, InfoValueSpanBuilder> linkifyValues(List<XmpProp> props) => {};
|
|
|
|
static String prefixForUri(Map<String, String> schemaRegistryPrefixes, String nsUri) => schemaRegistryPrefixes.entries.firstWhereOrNull((kv) => kv.value == nsUri)?.key ?? '';
|
|
}
|
|
|
|
class XmpProp implements Comparable<XmpProp> {
|
|
final String path, value;
|
|
final String displayKey;
|
|
|
|
XmpProp(this.path, this.value) : displayKey = formatKey(path);
|
|
|
|
static String formatKey(String propPath) {
|
|
return propPath.splitMapJoin(XMP.structFieldSeparator,
|
|
onMatch: (match) => ' ${match.group(0)} ',
|
|
onNonMatch: (s) {
|
|
// strip namespace
|
|
final key = s.split(XMP.propNamespaceSeparator).last;
|
|
// format
|
|
return key.replaceAll('_', ' ').toSentenceCase();
|
|
});
|
|
}
|
|
|
|
@override
|
|
int compareTo(XmpProp other) => compareAsciiUpperCaseNatural(displayKey, other.displayKey);
|
|
|
|
@override
|
|
String toString() => '$runtimeType#${shortHash(this)}{path=$path, value=$value}';
|
|
}
|
|
|
|
class XmpCardData {
|
|
final String title;
|
|
final RegExp pattern;
|
|
final bool indexed;
|
|
final Map<String, InfoValueSpanBuilder> Function(int?, Map<String, XmpProp> data)? spanBuilders;
|
|
final List<XmpCardData>? cards;
|
|
final Map<int?, XmpExtractedCard> data = {};
|
|
|
|
bool get isEmpty => data.isEmpty && (cards?.every((card) => card.isEmpty) ?? true);
|
|
|
|
static final titlePattern = RegExp(r'(.*?)[\\/]');
|
|
|
|
XmpCardData(
|
|
this.pattern, {
|
|
String? title,
|
|
this.spanBuilders,
|
|
this.cards,
|
|
}) : indexed = pattern.pattern.contains(r'\[(\d+)\]'),
|
|
title = title ?? XmpProp.formatKey(titlePattern.firstMatch(pattern.pattern)!.group(1)!);
|
|
|
|
XmpCardData cloneEmpty() {
|
|
return XmpCardData(
|
|
pattern,
|
|
title: title,
|
|
spanBuilders: spanBuilders,
|
|
cards: cards?.map((v) => v.cloneEmpty()).toList(),
|
|
);
|
|
}
|
|
|
|
bool extract(XmpProp prop) => indexed ? _extractIndexedStruct(prop) : _extractDirectStruct(prop);
|
|
|
|
bool _extractDirectStruct(XmpProp prop) {
|
|
final matches = pattern.allMatches(prop.path);
|
|
if (matches.isEmpty) return false;
|
|
|
|
final match = matches.first;
|
|
final field = match.group(1)!;
|
|
|
|
final fields = data.putIfAbsent(null, () => (<String, XmpProp>{}, cards?.map((v) => v.cloneEmpty()).toList()));
|
|
final _cards = fields.$2;
|
|
if (_cards != null) {
|
|
final fieldProp = XmpProp(field, prop.value);
|
|
if (_cards.any((v) => v.extract(fieldProp))) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
fields.$1[field] = prop;
|
|
return true;
|
|
}
|
|
|
|
bool _extractIndexedStruct(XmpProp prop) {
|
|
final matches = pattern.allMatches(prop.path);
|
|
if (matches.isEmpty) return false;
|
|
|
|
final match = matches.first;
|
|
final index = int.parse(match.group(1)!);
|
|
final field = match.group(2)!;
|
|
|
|
final fields = data.putIfAbsent(index, () => (<String, XmpProp>{}, cards?.map((v) => v.cloneEmpty()).toList()));
|
|
final _cards = fields.$2;
|
|
if (_cards != null) {
|
|
final fieldProp = XmpProp(field, prop.value);
|
|
if (_cards.any((v) => v.extract(fieldProp))) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
fields.$1[field] = prop;
|
|
return true;
|
|
}
|
|
}
|