info: improved display for XMP
This commit is contained in:
parent
47a2364f5a
commit
f687622997
17 changed files with 426 additions and 642 deletions
|
@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file.
|
||||||
- reverse filters to filter out/in
|
- reverse filters to filter out/in
|
||||||
- Collection: selection edit actions available as quick actions
|
- Collection: selection edit actions available as quick actions
|
||||||
- Albums: group by content type
|
- Albums: group by content type
|
||||||
|
- Info: improved display for XMP
|
||||||
- Stats: top albums
|
- Stats: top albums
|
||||||
- Stats: open full top listings
|
- Stats: open full top listings
|
||||||
- Slideshow: option for no transition
|
- Slideshow: option for no transition
|
||||||
|
|
|
@ -10,6 +10,7 @@ class Namespaces {
|
||||||
static const container = 'http://ns.google.com/photos/1.0/container/';
|
static const container = 'http://ns.google.com/photos/1.0/container/';
|
||||||
static const creatorAtom = 'http://ns.adobe.com/creatorAtom/1.0/';
|
static const creatorAtom = 'http://ns.adobe.com/creatorAtom/1.0/';
|
||||||
static const crd = 'http://ns.adobe.com/camera-raw-defaults/1.0/';
|
static const crd = 'http://ns.adobe.com/camera-raw-defaults/1.0/';
|
||||||
|
static const crlcp = 'http://ns.adobe.com/camera-raw-embedded-lens-profile/1.0/';
|
||||||
static const crs = 'http://ns.adobe.com/camera-raw-settings/1.0/';
|
static const crs = 'http://ns.adobe.com/camera-raw-settings/1.0/';
|
||||||
static const crss = 'http://ns.adobe.com/camera-raw-saved-settings/1.0/';
|
static const crss = 'http://ns.adobe.com/camera-raw-saved-settings/1.0/';
|
||||||
static const darktable = 'http://darktable.sf.net/';
|
static const darktable = 'http://darktable.sf.net/';
|
||||||
|
@ -30,7 +31,8 @@ class Namespaces {
|
||||||
static const gettyImagesGift = 'http://xmp.gettyimages.com/gift/1.0/';
|
static const gettyImagesGift = 'http://xmp.gettyimages.com/gift/1.0/';
|
||||||
static const gFocus = 'http://ns.google.com/photos/1.0/focus/';
|
static const gFocus = 'http://ns.google.com/photos/1.0/focus/';
|
||||||
static const gImage = 'http://ns.google.com/photos/1.0/image/';
|
static const gImage = 'http://ns.google.com/photos/1.0/image/';
|
||||||
static const gimp = 'http://www.gimp.org/ns/2.10/';
|
static const gimp210 = 'http://www.gimp.org/ns/2.10/';
|
||||||
|
static const gimpXmp = 'http://www.gimp.org/xmp/';
|
||||||
static const gPano = 'http://ns.google.com/photos/1.0/panorama/';
|
static const gPano = 'http://ns.google.com/photos/1.0/panorama/';
|
||||||
static const gSpherical = 'http://ns.google.com/videos/1.0/spherical/';
|
static const gSpherical = 'http://ns.google.com/videos/1.0/spherical/';
|
||||||
static const illustrator = 'http://ns.adobe.com/illustrator/1.0/';
|
static const illustrator = 'http://ns.adobe.com/illustrator/1.0/';
|
||||||
|
@ -56,6 +58,7 @@ class Namespaces {
|
||||||
static const plus = 'http://ns.useplus.org/ldf/xmp/1.0/';
|
static const plus = 'http://ns.useplus.org/ldf/xmp/1.0/';
|
||||||
static const pmtm = 'http://www.hdrsoft.com/photomatix_settings01';
|
static const pmtm = 'http://www.hdrsoft.com/photomatix_settings01';
|
||||||
static const rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
|
static const rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
|
||||||
|
static const stCamera = 'http://ns.adobe.com/photoshop/1.0/camera-profile';
|
||||||
static const stEvt = 'http://ns.adobe.com/xap/1.0/sType/ResourceEvent#';
|
static const stEvt = 'http://ns.adobe.com/xap/1.0/sType/ResourceEvent#';
|
||||||
static const stRef = 'http://ns.adobe.com/xap/1.0/sType/ResourceRef#';
|
static const stRef = 'http://ns.adobe.com/xap/1.0/sType/ResourceRef#';
|
||||||
static const tiff = 'http://ns.adobe.com/tiff/1.0/';
|
static const tiff = 'http://ns.adobe.com/tiff/1.0/';
|
||||||
|
@ -96,7 +99,8 @@ class Namespaces {
|
||||||
gDepth: 'Google Depth',
|
gDepth: 'Google Depth',
|
||||||
gFocus: 'Google Focus',
|
gFocus: 'Google Focus',
|
||||||
gImage: 'Google Image',
|
gImage: 'Google Image',
|
||||||
gimp: 'GIMP',
|
gimp210: 'GIMP 2.10',
|
||||||
|
gimpXmp: 'GIMP',
|
||||||
gPano: 'Google Panorama',
|
gPano: 'Google Panorama',
|
||||||
gSpherical: 'Google Spherical',
|
gSpherical: 'Google Spherical',
|
||||||
illustrator: 'Illustrator',
|
illustrator: 'Illustrator',
|
||||||
|
@ -122,6 +126,7 @@ class Namespaces {
|
||||||
xmpBJ: 'Basic Job Ticket',
|
xmpBJ: 'Basic Job Ticket',
|
||||||
xmpDM: 'Dynamic Media',
|
xmpDM: 'Dynamic Media',
|
||||||
xmpMM: 'Media Management',
|
xmpMM: 'Media Management',
|
||||||
|
xmpNote: 'Note',
|
||||||
xmpRights: 'Rights Management',
|
xmpRights: 'Rights Management',
|
||||||
xmpTPg: 'Paged-Text',
|
xmpTPg: 'Paged-Text',
|
||||||
};
|
};
|
||||||
|
|
157
lib/widgets/viewer/info/metadata/xmp_card.dart
Normal file
157
lib/widgets/viewer/info/metadata/xmp_card.dart
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:aves/theme/durations.dart';
|
||||||
|
import 'package:aves/theme/icons.dart';
|
||||||
|
import 'package:aves/utils/constants.dart';
|
||||||
|
import 'package:aves/widgets/common/basic/multi_cross_fader.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.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_namespaces.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
|
typedef XmpExtractedCard = Tuple2<Map<String, XmpProp>, List<XmpCardData>?>;
|
||||||
|
|
||||||
|
class XmpCard extends StatefulWidget {
|
||||||
|
final String title;
|
||||||
|
late final XmpExtractedCard? directStruct;
|
||||||
|
late final List<XmpExtractedCard>? indexedStructs;
|
||||||
|
final String Function(XmpProp prop) formatValue;
|
||||||
|
final Map<String, InfoValueSpanBuilder> Function(int? index)? spanBuilders;
|
||||||
|
|
||||||
|
XmpCard({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required Map<int?, XmpExtractedCard> structByIndex,
|
||||||
|
required this.formatValue,
|
||||||
|
this.spanBuilders,
|
||||||
|
}) {
|
||||||
|
directStruct = structByIndex[null];
|
||||||
|
|
||||||
|
final length = structByIndex.keys.whereNotNull().fold(0, max);
|
||||||
|
indexedStructs = length > 0 ? [for (var i = 0; i < length; i++) structByIndex[i + 1] ?? const Tuple2({}, null)] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<XmpCard> createState() => _XmpCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _XmpCardState extends State<XmpCard> {
|
||||||
|
final ValueNotifier<int> _indexNotifier = ValueNotifier(0);
|
||||||
|
|
||||||
|
List<XmpExtractedCard>? get indexedStructs => widget.indexedStructs;
|
||||||
|
|
||||||
|
bool get isIndexed => indexedStructs != null;
|
||||||
|
|
||||||
|
int get indexedStructCount => indexedStructs?.length ?? 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (isIndexed) {
|
||||||
|
_indexNotifier.value = indexedStructCount - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant XmpCard oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (_indexNotifier.value >= indexedStructCount) {
|
||||||
|
_indexNotifier.value = indexedStructCount - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final _isIndexed = isIndexed;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.onPrimary.withOpacity(.2),
|
||||||
|
),
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||||
|
),
|
||||||
|
child: ValueListenableBuilder<int>(
|
||||||
|
valueListenable: _indexNotifier,
|
||||||
|
builder: (context, index, child) {
|
||||||
|
final data = _isIndexed ? indexedStructs![index] : widget.directStruct!;
|
||||||
|
final props = data.item1.entries.map((kv) => XmpProp(kv.key, kv.value.value)).toList()..sort();
|
||||||
|
final cards = data.item2;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8, top: 8, right: 8),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: HighlightTitle(
|
||||||
|
title: widget.title,
|
||||||
|
selectable: true,
|
||||||
|
showHighlight: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_isIndexed) ...[
|
||||||
|
IconButton(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
icon: const Icon(AIcons.previous),
|
||||||
|
onPressed: index > 0 ? () => _setIndex(index - 1) : null,
|
||||||
|
tooltip: context.l10n.previousTooltip,
|
||||||
|
),
|
||||||
|
HighlightTitle(
|
||||||
|
title: '${index + 1}',
|
||||||
|
showHighlight: false,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
icon: const Icon(AIcons.next),
|
||||||
|
onPressed: index < indexedStructCount - 1 ? () => _setIndex(index + 1) : null,
|
||||||
|
tooltip: context.l10n.nextTooltip,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MultiCrossFader(
|
||||||
|
duration: Durations.xmpStructArrayCardTransition,
|
||||||
|
sizeCurve: Curves.easeOutBack,
|
||||||
|
alignment: AlignmentDirectional.topStart,
|
||||||
|
child: Padding(
|
||||||
|
// add padding at this level (instead of the column level)
|
||||||
|
// so that the crossfader can animate the content size
|
||||||
|
// without clipping the text
|
||||||
|
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||||
|
child: InfoRowGroup(
|
||||||
|
info: Map.fromEntries(props.map((prop) => MapEntry(prop.displayKey, widget.formatValue(prop)))),
|
||||||
|
maxValueLength: Constants.infoGroupMaxValueLength,
|
||||||
|
spanBuilders: widget.spanBuilders?.call(_isIndexed ? index + 1 : null),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (cards != null)
|
||||||
|
...cards.where((v) => !v.isEmpty).map((card) {
|
||||||
|
final spanBuilders = card.spanBuilders;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||||
|
child: XmpCard(
|
||||||
|
title: card.title,
|
||||||
|
structByIndex: card.data,
|
||||||
|
formatValue: widget.formatValue,
|
||||||
|
spanBuilders: spanBuilders != null ? (index) => spanBuilders(index, card.data[index]!.item1) : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setIndex(int index) => _indexNotifier.value = index.clamp(0, indexedStructCount - 1);
|
||||||
|
}
|
|
@ -5,17 +5,12 @@ import 'package:aves/utils/string_utils.dart';
|
||||||
import 'package:aves/utils/xmp_utils.dart';
|
import 'package:aves/utils/xmp_utils.dart';
|
||||||
import 'package:aves/widgets/common/identity/highlight_title.dart';
|
import 'package:aves/widgets/common/identity/highlight_title.dart';
|
||||||
import 'package:aves/widgets/viewer/info/common.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/crs.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/darktable.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/dwc.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/exif.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/google.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/iptc.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/misc.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/iptc4xmpext.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/microsoft.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/mwg.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/photoshop.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/photoshop.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/plus.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/tiff.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:aves/widgets/viewer/info/metadata/xmp_ns/xmp.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
@ -23,6 +18,7 @@ import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class XmpNamespace extends Equatable {
|
class XmpNamespace extends Equatable {
|
||||||
|
@ -70,8 +66,6 @@ class XmpNamespace extends Equatable {
|
||||||
return XmpBasicNamespace(nsPrefix, rawProps);
|
return XmpBasicNamespace(nsPrefix, rawProps);
|
||||||
case Namespaces.xmpMM:
|
case Namespaces.xmpMM:
|
||||||
return XmpMMNamespace(nsPrefix, rawProps);
|
return XmpMMNamespace(nsPrefix, rawProps);
|
||||||
case Namespaces.xmpNote:
|
|
||||||
return XmpNoteNamespace(nsPrefix, rawProps);
|
|
||||||
default:
|
default:
|
||||||
return XmpNamespace(nsUri, nsPrefix, rawProps);
|
return XmpNamespace(nsUri, nsPrefix, rawProps);
|
||||||
}
|
}
|
||||||
|
@ -79,26 +73,37 @@ class XmpNamespace extends Equatable {
|
||||||
|
|
||||||
String get displayTitle => Namespaces.nsTitles[nsUri] ?? (nsPrefix.isEmpty ? nsUri : '${nsPrefix.substring(0, nsPrefix.length - 1)} ($nsUri)');
|
String get displayTitle => Namespaces.nsTitles[nsUri] ?? (nsPrefix.isEmpty ? nsUri : '${nsPrefix.substring(0, nsPrefix.length - 1)} ($nsUri)');
|
||||||
|
|
||||||
Map<String, String> get buildProps => rawProps;
|
|
||||||
|
|
||||||
List<Widget> buildNamespaceSection(BuildContext context) {
|
List<Widget> buildNamespaceSection(BuildContext context) {
|
||||||
final props = buildProps.entries
|
final props = rawProps.entries
|
||||||
.map((kv) {
|
.map((kv) {
|
||||||
final prop = XmpProp(kv.key, kv.value);
|
final prop = XmpProp(kv.key, kv.value);
|
||||||
return extractData(prop) ? null : prop;
|
var extracted = false;
|
||||||
|
cards.forEach((card) => extracted |= card.extract(prop));
|
||||||
|
return extracted ? null : prop;
|
||||||
})
|
})
|
||||||
.whereNotNull()
|
.whereNotNull()
|
||||||
.toList()
|
.toList()
|
||||||
..sort((a, b) => compareAsciiUpperCaseNatural(a.displayKey, b.displayKey));
|
..sort();
|
||||||
|
|
||||||
final content = [
|
final content = [
|
||||||
if (props.isNotEmpty)
|
if (props.isNotEmpty)
|
||||||
InfoRowGroup(
|
InfoRowGroup(
|
||||||
info: Map.fromEntries(props.map((prop) => MapEntry(prop.displayKey, formatValue(prop)))),
|
info: Map.fromEntries(props.map((v) => MapEntry(v.displayKey, formatValue(v)))),
|
||||||
maxValueLength: Constants.infoGroupMaxValueLength,
|
maxValueLength: Constants.infoGroupMaxValueLength,
|
||||||
spanBuilders: linkifyValues(props),
|
spanBuilders: linkifyValues(props),
|
||||||
),
|
),
|
||||||
...buildFromExtractedData(),
|
...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]!.item1) : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
return content.isNotEmpty
|
return content.isNotEmpty
|
||||||
|
@ -117,38 +122,14 @@ class XmpNamespace extends Equatable {
|
||||||
: [];
|
: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
bool extractStruct(XmpProp prop, RegExp pattern, Map<String, String> store) {
|
List<XmpCardData> get cards => [];
|
||||||
final matches = pattern.allMatches(prop.path);
|
|
||||||
if (matches.isEmpty) return false;
|
|
||||||
|
|
||||||
final match = matches.first;
|
|
||||||
final field = match.group(1)!;
|
|
||||||
store[field] = formatValue(prop);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool extractIndexedStruct(XmpProp prop, RegExp pattern, Map<int, Map<String, String>> store) {
|
|
||||||
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 = store.putIfAbsent(index, () => <String, String>{});
|
|
||||||
fields[field] = formatValue(prop);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool extractData(XmpProp prop) => false;
|
|
||||||
|
|
||||||
List<Widget> buildFromExtractedData() => [];
|
|
||||||
|
|
||||||
String formatValue(XmpProp prop) => prop.value;
|
String formatValue(XmpProp prop) => prop.value;
|
||||||
|
|
||||||
Map<String, InfoValueSpanBuilder> linkifyValues(List<XmpProp> props) => {};
|
Map<String, InfoValueSpanBuilder> linkifyValues(List<XmpProp> props) => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
class XmpProp {
|
class XmpProp implements Comparable<XmpProp> {
|
||||||
final String path, value;
|
final String path, value;
|
||||||
final String displayKey;
|
final String displayKey;
|
||||||
|
|
||||||
|
@ -165,6 +146,82 @@ class XmpProp {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int compareTo(XmpProp other) => compareAsciiUpperCaseNatural(displayKey, other.displayKey);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$runtimeType#${shortHash(this)}{path=$path, value=$value}';
|
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, () => Tuple2(<String, XmpProp>{}, cards?.map((v) => v.cloneEmpty()).toList()));
|
||||||
|
final _cards = fields.item2;
|
||||||
|
if (_cards != null) {
|
||||||
|
final fieldProp = XmpProp(field, prop.value);
|
||||||
|
if (_cards.any((v) => v.extract(fieldProp))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fields.item1[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, () => Tuple2(<String, XmpProp>{}, cards?.map((v) => v.cloneEmpty()).toList()));
|
||||||
|
final _cards = fields.item2;
|
||||||
|
if (_cards != null) {
|
||||||
|
final fieldProp = XmpProp(field, prop.value);
|
||||||
|
if (_cards.any((v) => v.extract(fieldProp))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fields.item1[field] = prop;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,75 +1,50 @@
|
||||||
import 'package:aves/utils/xmp_utils.dart';
|
import 'package:aves/utils/xmp_utils.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
class XmpCrsNamespace extends XmpNamespace {
|
class XmpCrsNamespace extends XmpNamespace {
|
||||||
late final cgbcPattern = RegExp(nsPrefix + r'CircularGradientBasedCorrections\[(\d+)\]/(.*)');
|
|
||||||
late final gbcPattern = RegExp(nsPrefix + r'GradientBasedCorrections\[(\d+)\]/(.*)');
|
|
||||||
late final mgbcPattern = RegExp(nsPrefix + r'MaskGroupBasedCorrections\[(\d+)\]/(.*)');
|
|
||||||
late final pbcPattern = RegExp(nsPrefix + r'PaintBasedCorrections\[(\d+)\]/(.*)');
|
|
||||||
late final retouchAreasPattern = RegExp(nsPrefix + r'RetouchAreas\[(\d+)\]/(.*)');
|
|
||||||
late final lookPattern = RegExp(nsPrefix + r'Look/(.*)');
|
|
||||||
late final rmmiPattern = RegExp(nsPrefix + r'RangeMaskMapInfo/' + nsPrefix + r'RangeMaskMapInfo/(.*)');
|
|
||||||
|
|
||||||
final cgbc = <int, Map<String, String>>{};
|
|
||||||
final gbc = <int, Map<String, String>>{};
|
|
||||||
final mgbc = <int, Map<String, String>>{};
|
|
||||||
final pbc = <int, Map<String, String>>{};
|
|
||||||
final retouchAreas = <int, Map<String, String>>{};
|
|
||||||
final look = <String, String>{};
|
|
||||||
final rmmi = <String, String>{};
|
|
||||||
|
|
||||||
XmpCrsNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.crs, nsPrefix, rawProps);
|
XmpCrsNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.crs, nsPrefix, rawProps);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool extractData(XmpProp prop) {
|
late final List<XmpCardData> cards = [
|
||||||
var hasStructs = extractStruct(prop, lookPattern, look);
|
XmpCardData(RegExp(nsPrefix + r'CircularGradientBasedCorrections\[(\d+)\]/(.*)')),
|
||||||
hasStructs |= extractStruct(prop, rmmiPattern, rmmi);
|
XmpCardData(
|
||||||
var hasIndexedStructs = extractIndexedStruct(prop, cgbcPattern, cgbc);
|
RegExp(nsPrefix + r'GradientBasedCorrections\[(\d+)\]/(.*)'),
|
||||||
hasIndexedStructs |= extractIndexedStruct(prop, gbcPattern, gbc);
|
cards: [
|
||||||
hasIndexedStructs |= extractIndexedStruct(prop, mgbcPattern, mgbc);
|
XmpCardData(RegExp(nsPrefix + r'CorrectionMasks\[(\d+)\]/(.*)')),
|
||||||
hasIndexedStructs |= extractIndexedStruct(prop, pbcPattern, pbc);
|
XmpCardData(RegExp(nsPrefix + r'CorrectionRangeMask/(.*)')),
|
||||||
hasIndexedStructs |= extractIndexedStruct(prop, retouchAreasPattern, retouchAreas);
|
],
|
||||||
return hasStructs || hasIndexedStructs;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Widget> buildFromExtractedData() => [
|
|
||||||
if (cgbc.isNotEmpty)
|
|
||||||
XmpStructArrayCard(
|
|
||||||
title: 'Circular Gradient Based Corrections',
|
|
||||||
structByIndex: cgbc,
|
|
||||||
),
|
),
|
||||||
if (gbc.isNotEmpty)
|
XmpCardData(
|
||||||
XmpStructArrayCard(
|
RegExp(nsPrefix + r'Look/(.*)'),
|
||||||
title: 'Gradient Based Corrections',
|
cards: [
|
||||||
structByIndex: gbc,
|
XmpCardData(RegExp(nsPrefix + r'Parameters/(.*)')),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
if (look.isNotEmpty)
|
XmpCardData(
|
||||||
XmpStructCard(
|
RegExp(nsPrefix + r'MaskGroupBasedCorrections\[(\d+)\]/(.*)'),
|
||||||
title: 'Look',
|
cards: [
|
||||||
struct: look,
|
XmpCardData(
|
||||||
|
RegExp(nsPrefix + r'CorrectionMasks\[(\d+)\]/(.*)'),
|
||||||
|
cards: [
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'CorrectionRangeMask/(.*)')),
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'Masks\[(\d+)\]/(.*)')),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
if (mgbc.isNotEmpty)
|
],
|
||||||
XmpStructArrayCard(
|
|
||||||
title: 'Mask Group Based Corrections',
|
|
||||||
structByIndex: mgbc,
|
|
||||||
),
|
),
|
||||||
if (pbc.isNotEmpty)
|
XmpCardData(
|
||||||
XmpStructArrayCard(
|
RegExp(nsPrefix + r'PaintBasedCorrections\[(\d+)\]/(.*)'),
|
||||||
title: 'Paint Based Corrections',
|
cards: [
|
||||||
structByIndex: pbc,
|
XmpCardData(RegExp(nsPrefix + r'CorrectionMasks\[(\d+)\]/(.*)')),
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'CorrectionRangeMask/(.*)')),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
if (rmmi.isNotEmpty)
|
XmpCardData(
|
||||||
XmpStructCard(
|
RegExp(nsPrefix + r'RetouchAreas\[(\d+)\]/(.*)'),
|
||||||
title: 'Range Mask Map Info',
|
cards: [
|
||||||
struct: rmmi,
|
XmpCardData(RegExp(nsPrefix + r'Masks\[(\d+)\]/(.*)')),
|
||||||
),
|
],
|
||||||
if (retouchAreas.isNotEmpty)
|
|
||||||
XmpStructArrayCard(
|
|
||||||
title: 'Retouch Areas',
|
|
||||||
structByIndex: retouchAreas,
|
|
||||||
),
|
),
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'RangeMaskMapInfo/' + nsPrefix + r'RangeMaskMapInfo/(.*)')),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
import 'package:aves/utils/xmp_utils.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class XmpDarktableNamespace extends XmpNamespace {
|
|
||||||
late final historyPattern = RegExp(nsPrefix + r'history\[(\d+)\]/(.*)');
|
|
||||||
|
|
||||||
final history = <int, Map<String, String>>{};
|
|
||||||
|
|
||||||
XmpDarktableNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.darktable, nsPrefix, rawProps);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool extractData(XmpProp prop) => extractIndexedStruct(prop, historyPattern, history);
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Widget> buildFromExtractedData() => [
|
|
||||||
if (history.isNotEmpty)
|
|
||||||
XmpStructArrayCard(
|
|
||||||
title: 'History',
|
|
||||||
structByIndex: history,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
import 'package:aves/utils/xmp_utils.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
class XmpDwcNamespace extends XmpNamespace {
|
|
||||||
late final dcTermsLocationPattern = RegExp(nsPrefix + r'dctermsLocation/(.*)');
|
|
||||||
late final eventPattern = RegExp(nsPrefix + r'Event/(.*)');
|
|
||||||
late final geologicalContextPattern = RegExp(nsPrefix + r'GeologicalContext/(.*)');
|
|
||||||
late final identificationPattern = RegExp(nsPrefix + r'Identification/(.*)');
|
|
||||||
late final measurementOrFactPattern = RegExp(nsPrefix + r'MeasurementOrFact/(.*)');
|
|
||||||
late final occurrencePattern = RegExp(nsPrefix + r'Occurrence/(.*)');
|
|
||||||
late final recordPattern = RegExp(nsPrefix + r'Record/(.*)');
|
|
||||||
late final resourceRelationshipPattern = RegExp(nsPrefix + r'ResourceRelationship/(.*)');
|
|
||||||
late final taxonPattern = RegExp(nsPrefix + r'Taxon/(.*)');
|
|
||||||
|
|
||||||
final dcTermsLocation = <String, String>{};
|
|
||||||
final event = <String, String>{};
|
|
||||||
final identification = <String, String>{};
|
|
||||||
final geologicalContext = <String, String>{};
|
|
||||||
final measurementOrFact = <String, String>{};
|
|
||||||
final occurrence = <String, String>{};
|
|
||||||
final record = <String, String>{};
|
|
||||||
final resourceRelationship = <String, String>{};
|
|
||||||
final taxon = <String, String>{};
|
|
||||||
|
|
||||||
XmpDwcNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.dwc, nsPrefix, rawProps);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool extractData(XmpProp prop) {
|
|
||||||
var hasStructs = extractStruct(prop, dcTermsLocationPattern, dcTermsLocation);
|
|
||||||
hasStructs |= extractStruct(prop, eventPattern, event);
|
|
||||||
hasStructs |= extractStruct(prop, geologicalContextPattern, geologicalContext);
|
|
||||||
hasStructs |= extractStruct(prop, measurementOrFactPattern, measurementOrFact);
|
|
||||||
hasStructs |= extractStruct(prop, identificationPattern, identification);
|
|
||||||
hasStructs |= extractStruct(prop, occurrencePattern, occurrence);
|
|
||||||
hasStructs |= extractStruct(prop, recordPattern, record);
|
|
||||||
hasStructs |= extractStruct(prop, resourceRelationshipPattern, resourceRelationship);
|
|
||||||
hasStructs |= extractStruct(prop, taxonPattern, taxon);
|
|
||||||
return hasStructs;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Widget> buildFromExtractedData() => [
|
|
||||||
if (dcTermsLocation.isNotEmpty)
|
|
||||||
XmpStructCard(
|
|
||||||
title: 'DC Terms Location',
|
|
||||||
struct: dcTermsLocation,
|
|
||||||
),
|
|
||||||
if (event.isNotEmpty)
|
|
||||||
XmpStructCard(
|
|
||||||
title: 'Event',
|
|
||||||
struct: event,
|
|
||||||
),
|
|
||||||
if (geologicalContext.isNotEmpty)
|
|
||||||
XmpStructCard(
|
|
||||||
title: 'Geological Context',
|
|
||||||
struct: geologicalContext,
|
|
||||||
),
|
|
||||||
if (identification.isNotEmpty)
|
|
||||||
XmpStructCard(
|
|
||||||
title: 'Identification',
|
|
||||||
struct: identification,
|
|
||||||
),
|
|
||||||
if (measurementOrFact.isNotEmpty)
|
|
||||||
XmpStructCard(
|
|
||||||
title: 'Measurement Or Fact',
|
|
||||||
struct: measurementOrFact,
|
|
||||||
),
|
|
||||||
if (occurrence.isNotEmpty)
|
|
||||||
XmpStructCard(
|
|
||||||
title: 'Occurrence',
|
|
||||||
struct: occurrence,
|
|
||||||
),
|
|
||||||
if (record.isNotEmpty)
|
|
||||||
XmpStructCard(
|
|
||||||
title: 'Record',
|
|
||||||
struct: record,
|
|
||||||
),
|
|
||||||
if (resourceRelationship.isNotEmpty)
|
|
||||||
XmpStructCard(
|
|
||||||
title: 'Resource Relationship',
|
|
||||||
struct: resourceRelationship,
|
|
||||||
),
|
|
||||||
if (taxon.isNotEmpty)
|
|
||||||
XmpStructCard(
|
|
||||||
title: 'Taxon',
|
|
||||||
struct: taxon,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
|
@ -3,9 +3,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/viewer/embedded/notifications.dart';
|
import 'package:aves/widgets/viewer/embedded/notifications.dart';
|
||||||
import 'package:aves/widgets/viewer/info/common.dart';
|
import 'package:aves/widgets/viewer/info/common.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
abstract class XmpGoogleNamespace extends XmpNamespace {
|
abstract class XmpGoogleNamespace extends XmpNamespace {
|
||||||
|
@ -79,21 +77,10 @@ class XmpGImageNamespace extends XmpGoogleNamespace {
|
||||||
}
|
}
|
||||||
|
|
||||||
class XmpContainer extends XmpNamespace {
|
class XmpContainer extends XmpNamespace {
|
||||||
late final directoryPattern = RegExp('${nsPrefix}Directory\\[(\\d+)\\]/${nsPrefix}Item/(.*)');
|
|
||||||
|
|
||||||
final directories = <int, Map<String, String>>{};
|
|
||||||
|
|
||||||
XmpContainer(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.container, nsPrefix, rawProps);
|
XmpContainer(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.container, nsPrefix, rawProps);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool extractData(XmpProp prop) => extractIndexedStruct(prop, directoryPattern, directories);
|
late final List<XmpCardData> cards = [
|
||||||
|
XmpCardData(RegExp('${nsPrefix}Directory\\[(\\d+)\\]/${nsPrefix}Item/(.*)'), title: 'Directory Item'),
|
||||||
@override
|
|
||||||
List<Widget> buildFromExtractedData() => [
|
|
||||||
if (directories.isNotEmpty)
|
|
||||||
XmpStructArrayCard(
|
|
||||||
title: 'Directory Item',
|
|
||||||
structByIndex: directories,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
import 'package:aves/utils/xmp_utils.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class XmpIptcCoreNamespace extends XmpNamespace {
|
|
||||||
late final creatorContactInfoPattern = RegExp(nsPrefix + r'CreatorContactInfo/(.*)');
|
|
||||||
|
|
||||||
final creatorContactInfo = <String, String>{};
|
|
||||||
|
|
||||||
XmpIptcCoreNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.iptc4xmpCore, nsPrefix, rawProps);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool extractData(XmpProp prop) => extractStruct(prop, creatorContactInfoPattern, creatorContactInfo);
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Widget> buildFromExtractedData() => [
|
|
||||||
if (creatorContactInfo.isNotEmpty)
|
|
||||||
XmpStructCard(
|
|
||||||
title: 'Creator Contact Info',
|
|
||||||
struct: creatorContactInfo,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
import 'package:aves/utils/xmp_utils.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class XmpIptc4xmpExtNamespace extends XmpNamespace {
|
|
||||||
late final aooPattern = RegExp(nsPrefix + r'ArtworkOrObject\[(\d+)\]/(.*)');
|
|
||||||
|
|
||||||
final aoo = <int, Map<String, String>>{};
|
|
||||||
|
|
||||||
XmpIptc4xmpExtNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.iptc4xmpExt, nsPrefix, rawProps);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool extractData(XmpProp prop) => extractIndexedStruct(prop, aooPattern, aoo);
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Widget> buildFromExtractedData() => [
|
|
||||||
if (aoo.isNotEmpty)
|
|
||||||
XmpStructArrayCard(
|
|
||||||
title: 'Artwork or Object',
|
|
||||||
structByIndex: aoo,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
import 'package:aves/utils/xmp_utils.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
class XmpMPNamespace extends XmpNamespace {
|
|
||||||
late final regionListPattern = RegExp(nsPrefix + r'RegionInfo/MPRI:Regions\[(\d+)\]/(.*)');
|
|
||||||
|
|
||||||
final regionList = <int, Map<String, String>>{};
|
|
||||||
|
|
||||||
XmpMPNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.mp, nsPrefix, rawProps);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool extractData(XmpProp prop) => extractIndexedStruct(prop, regionListPattern, regionList);
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Widget> buildFromExtractedData() => [
|
|
||||||
if (regionList.isNotEmpty)
|
|
||||||
XmpStructArrayCard(
|
|
||||||
title: 'Regions',
|
|
||||||
structByIndex: regionList,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
87
lib/widgets/viewer/info/metadata/xmp_ns/misc.dart
Normal file
87
lib/widgets/viewer/info/metadata/xmp_ns/misc.dart
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import 'package:aves/utils/xmp_utils.dart';
|
||||||
|
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
||||||
|
|
||||||
|
class XmpDarktableNamespace extends XmpNamespace {
|
||||||
|
XmpDarktableNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.darktable, nsPrefix, rawProps);
|
||||||
|
|
||||||
|
@override
|
||||||
|
late final List<XmpCardData> cards = [
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'history\[(\d+)\]/(.*)')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
class XmpDwcNamespace extends XmpNamespace {
|
||||||
|
XmpDwcNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.dwc, nsPrefix, rawProps);
|
||||||
|
|
||||||
|
@override
|
||||||
|
late final List<XmpCardData> cards = [
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'dctermsLocation/(.*)'), title: 'DC Terms Location'),
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'Event/(.*)')),
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'GeologicalContext/(.*)')),
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'Identification/(.*)')),
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'MeasurementOrFact/(.*)')),
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'Occurrence/(.*)')),
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'Record/(.*)')),
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'ResourceRelationship/(.*)')),
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'Taxon/(.*)')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
class XmpIptcCoreNamespace extends XmpNamespace {
|
||||||
|
XmpIptcCoreNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.iptc4xmpCore, nsPrefix, rawProps);
|
||||||
|
|
||||||
|
@override
|
||||||
|
late final List<XmpCardData> cards = [
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'CreatorContactInfo/(.*)')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
class XmpIptc4xmpExtNamespace extends XmpNamespace {
|
||||||
|
XmpIptc4xmpExtNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.iptc4xmpExt, nsPrefix, rawProps);
|
||||||
|
|
||||||
|
@override
|
||||||
|
late final List<XmpCardData> cards = [
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'ArtworkOrObject\[(\d+)\]/(.*)')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
class XmpMPNamespace extends XmpNamespace {
|
||||||
|
XmpMPNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.mp, nsPrefix, rawProps);
|
||||||
|
|
||||||
|
@override
|
||||||
|
late final List<XmpCardData> cards = [
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'RegionInfo/MPRI:Regions\[(\d+)\]/(.*)'), title: 'Regions'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// cf www.metadataworkinggroup.org/pdf/mwg_guidance.pdf (down, as of 2021/02/15)
|
||||||
|
class XmpMgwRegionsNamespace extends XmpNamespace {
|
||||||
|
XmpMgwRegionsNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.mwgrs, nsPrefix, rawProps);
|
||||||
|
|
||||||
|
@override
|
||||||
|
late final List<XmpCardData> cards = [
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'Regions/mwg-rs:AppliedToDimensions/(.*)'), title: 'Applied to Dimensions'),
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'Regions/mwg-rs:RegionList\[(\d+)\]/(.*)'), title: 'Region List'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
class XmpPlusNamespace extends XmpNamespace {
|
||||||
|
XmpPlusNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.plus, nsPrefix, rawProps);
|
||||||
|
|
||||||
|
@override
|
||||||
|
late final List<XmpCardData> cards = [
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'Licensor\[(\d+)\]/(.*)')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
class XmpMMNamespace extends XmpNamespace {
|
||||||
|
XmpMMNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.xmpMM, nsPrefix, rawProps);
|
||||||
|
|
||||||
|
@override
|
||||||
|
late final List<XmpCardData> cards = [
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'DerivedFrom/(.*)')),
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'History\[(\d+)\]/(.*)')),
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'Ingredients\[(\d+)\]/(.*)')),
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'Pantry\[(\d+)\]/(.*)')),
|
||||||
|
];
|
||||||
|
}
|
|
@ -1,36 +0,0 @@
|
||||||
import 'package:aves/utils/xmp_utils.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
// cf www.metadataworkinggroup.org/pdf/mwg_guidance.pdf (down, as of 2021/02/15)
|
|
||||||
class XmpMgwRegionsNamespace extends XmpNamespace {
|
|
||||||
late final dimensionsPattern = RegExp(nsPrefix + r'Regions/mwg-rs:AppliedToDimensions/(.*)');
|
|
||||||
late final regionListPattern = RegExp(nsPrefix + r'Regions/mwg-rs:RegionList\[(\d+)\]/(.*)');
|
|
||||||
|
|
||||||
final dimensions = <String, String>{};
|
|
||||||
final regionList = <int, Map<String, String>>{};
|
|
||||||
|
|
||||||
XmpMgwRegionsNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.mwgrs, nsPrefix, rawProps);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool extractData(XmpProp prop) {
|
|
||||||
final hasStructs = extractStruct(prop, dimensionsPattern, dimensions);
|
|
||||||
final hasIndexedStructs = extractIndexedStruct(prop, regionListPattern, regionList);
|
|
||||||
return hasStructs || hasIndexedStructs;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Widget> buildFromExtractedData() => [
|
|
||||||
if (dimensions.isNotEmpty)
|
|
||||||
XmpStructCard(
|
|
||||||
title: 'Applied To Dimensions',
|
|
||||||
struct: dimensions,
|
|
||||||
),
|
|
||||||
if (regionList.isNotEmpty)
|
|
||||||
XmpStructArrayCard(
|
|
||||||
title: 'Region',
|
|
||||||
structByIndex: regionList,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
|
@ -1,37 +1,19 @@
|
||||||
import 'package:aves/utils/xmp_utils.dart';
|
import 'package:aves/utils/xmp_utils.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
// cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/photoshop.md
|
// cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/photoshop.md
|
||||||
class XmpPhotoshopNamespace extends XmpNamespace {
|
class XmpPhotoshopNamespace extends XmpNamespace {
|
||||||
late final cameraProfilesPattern = RegExp(nsPrefix + r'CameraProfiles\[(\d+)\]/(.*)');
|
|
||||||
late final textLayersPattern = RegExp(nsPrefix + r'TextLayers\[(\d+)\]/(.*)');
|
|
||||||
|
|
||||||
final cameraProfiles = <int, Map<String, String>>{};
|
|
||||||
final textLayers = <int, Map<String, String>>{};
|
|
||||||
|
|
||||||
XmpPhotoshopNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.photoshop, nsPrefix, rawProps);
|
XmpPhotoshopNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.photoshop, nsPrefix, rawProps);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool extractData(XmpProp prop) {
|
late final List<XmpCardData> cards = [
|
||||||
var hasIndexedStructs = extractIndexedStruct(prop, cameraProfilesPattern, cameraProfiles);
|
XmpCardData(
|
||||||
hasIndexedStructs |= extractIndexedStruct(prop, textLayersPattern, textLayers);
|
RegExp(nsPrefix + r'CameraProfiles\[(\d+)\]/(.*)'),
|
||||||
return hasIndexedStructs;
|
cards: [
|
||||||
}
|
XmpCardData(RegExp(r'crlcp:PerspectiveModel/(.*)')),
|
||||||
|
],
|
||||||
@override
|
|
||||||
List<Widget> buildFromExtractedData() => [
|
|
||||||
if (cameraProfiles.isNotEmpty)
|
|
||||||
XmpStructArrayCard(
|
|
||||||
title: 'Camera Profiles',
|
|
||||||
structByIndex: cameraProfiles,
|
|
||||||
),
|
|
||||||
if (textLayers.isNotEmpty)
|
|
||||||
XmpStructArrayCard(
|
|
||||||
title: 'Text Layers',
|
|
||||||
structByIndex: textLayers,
|
|
||||||
),
|
),
|
||||||
|
XmpCardData(RegExp(nsPrefix + r'TextLayers\[(\d+)\]/(.*)')),
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
import 'package:aves/utils/xmp_utils.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class XmpPlusNamespace extends XmpNamespace {
|
|
||||||
late final licensorPattern = RegExp(nsPrefix + r'Licensor\[(\d+)\]/(.*)');
|
|
||||||
|
|
||||||
final licensor = <int, Map<String, String>>{};
|
|
||||||
|
|
||||||
XmpPlusNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.plus, nsPrefix, rawProps);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool extractData(XmpProp prop) => extractIndexedStruct(prop, licensorPattern, licensor);
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Widget> buildFromExtractedData() => [
|
|
||||||
if (licensor.isNotEmpty)
|
|
||||||
XmpStructArrayCard(
|
|
||||||
title: 'Licensor',
|
|
||||||
structByIndex: licensor,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
|
@ -4,31 +4,18 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/viewer/embedded/notifications.dart';
|
import 'package:aves/widgets/viewer/embedded/notifications.dart';
|
||||||
import 'package:aves/widgets/viewer/info/common.dart';
|
import 'package:aves/widgets/viewer/info/common.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class XmpBasicNamespace extends XmpNamespace {
|
class XmpBasicNamespace extends XmpNamespace {
|
||||||
late final thumbnailsPattern = RegExp(nsPrefix + r'Thumbnails\[(\d+)\]/(.*)');
|
|
||||||
static const thumbnailDataDisplayKey = 'Image';
|
|
||||||
|
|
||||||
final thumbnails = <int, Map<String, String>>{};
|
|
||||||
|
|
||||||
XmpBasicNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.xmp, nsPrefix, rawProps);
|
XmpBasicNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.xmp, nsPrefix, rawProps);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool extractData(XmpProp prop) => extractIndexedStruct(prop, thumbnailsPattern, thumbnails);
|
late final List<XmpCardData> cards = [
|
||||||
|
XmpCardData(
|
||||||
@override
|
RegExp(nsPrefix + r'Thumbnails\[(\d+)\]/(.*)'),
|
||||||
List<Widget> buildFromExtractedData() => [
|
spanBuilders: (index, struct) {
|
||||||
if (thumbnails.isNotEmpty)
|
|
||||||
XmpStructArrayCard(
|
|
||||||
title: 'Thumbnail',
|
|
||||||
structByIndex: thumbnails,
|
|
||||||
linkifier: (index) {
|
|
||||||
final struct = thumbnails[index]!;
|
|
||||||
return {
|
return {
|
||||||
if (struct.containsKey(thumbnailDataDisplayKey))
|
if (struct.containsKey('xmpGImg:image'))
|
||||||
thumbnailDataDisplayKey: InfoRowGroup.linkSpanBuilder(
|
'Image': InfoRowGroup.linkSpanBuilder(
|
||||||
linkText: (context) => context.l10n.viewerInfoOpenLinkText,
|
linkText: (context) => context.l10n.viewerInfoOpenLinkText,
|
||||||
onTap: (context) => OpenEmbeddedDataNotification.xmp(
|
onTap: (context) => OpenEmbeddedDataNotification.xmp(
|
||||||
props: [
|
props: [
|
||||||
|
@ -41,65 +28,6 @@ class XmpBasicNamespace extends XmpNamespace {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
class XmpMMNamespace extends XmpNamespace {
|
|
||||||
late final derivedFromPattern = RegExp(nsPrefix + r'DerivedFrom/(.*)');
|
|
||||||
late final historyPattern = RegExp(nsPrefix + r'History\[(\d+)\]/(.*)');
|
|
||||||
late final ingredientsPattern = RegExp(nsPrefix + r'Ingredients\[(\d+)\]/(.*)');
|
|
||||||
late final pantryPattern = RegExp(nsPrefix + r'Pantry\[(\d+)\]/(.*)');
|
|
||||||
|
|
||||||
final derivedFrom = <String, String>{};
|
|
||||||
final history = <int, Map<String, String>>{};
|
|
||||||
final ingredients = <int, Map<String, String>>{};
|
|
||||||
final pantry = <int, Map<String, String>>{};
|
|
||||||
|
|
||||||
XmpMMNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.xmpMM, nsPrefix, rawProps);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool extractData(XmpProp prop) {
|
|
||||||
final hasStructs = extractStruct(prop, derivedFromPattern, derivedFrom);
|
|
||||||
var hasIndexedStructs = extractIndexedStruct(prop, historyPattern, history);
|
|
||||||
hasIndexedStructs |= extractIndexedStruct(prop, ingredientsPattern, ingredients);
|
|
||||||
hasIndexedStructs |= extractIndexedStruct(prop, pantryPattern, pantry);
|
|
||||||
return hasStructs || hasIndexedStructs;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Widget> buildFromExtractedData() => [
|
|
||||||
if (derivedFrom.isNotEmpty)
|
|
||||||
XmpStructCard(
|
|
||||||
title: 'Derived From',
|
|
||||||
struct: derivedFrom,
|
|
||||||
),
|
|
||||||
if (history.isNotEmpty)
|
|
||||||
XmpStructArrayCard(
|
|
||||||
title: 'History',
|
|
||||||
structByIndex: history,
|
|
||||||
),
|
|
||||||
if (ingredients.isNotEmpty)
|
|
||||||
XmpStructArrayCard(
|
|
||||||
title: 'Ingredients',
|
|
||||||
structByIndex: ingredients,
|
|
||||||
),
|
|
||||||
if (pantry.isNotEmpty)
|
|
||||||
XmpStructArrayCard(
|
|
||||||
title: 'Pantry',
|
|
||||||
structByIndex: pantry,
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
class XmpNoteNamespace extends XmpNamespace {
|
|
||||||
// `xmpNote:HasExtendedXMP` is structural and should not be displayed to users
|
|
||||||
late final hasExtendedXmp = '${nsPrefix}HasExtendedXMP';
|
|
||||||
|
|
||||||
XmpNoteNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.xmpNote, nsPrefix, rawProps);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool extractData(XmpProp prop) {
|
|
||||||
return prop.path == hasExtendedXmp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,148 +0,0 @@
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:aves/theme/durations.dart';
|
|
||||||
import 'package:aves/theme/icons.dart';
|
|
||||||
import 'package:aves/theme/themes.dart';
|
|
||||||
import 'package:aves/utils/constants.dart';
|
|
||||||
import 'package:aves/widgets/common/basic/multi_cross_fader.dart';
|
|
||||||
import 'package:aves/widgets/common/extensions/build_context.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_namespaces.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class XmpStructArrayCard extends StatefulWidget {
|
|
||||||
final String title;
|
|
||||||
late final List<Map<String, String>> structs;
|
|
||||||
final Map<String, InfoValueSpanBuilder> Function(int index)? linkifier;
|
|
||||||
|
|
||||||
XmpStructArrayCard({
|
|
||||||
super.key,
|
|
||||||
required this.title,
|
|
||||||
required Map<int, Map<String, String>> structByIndex,
|
|
||||||
this.linkifier,
|
|
||||||
}) {
|
|
||||||
final length = structByIndex.keys.fold(0, max);
|
|
||||||
structs = [for (var i = 0; i < length; i++) structByIndex[i + 1] ?? {}];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<XmpStructArrayCard> createState() => _XmpStructArrayCardState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _XmpStructArrayCardState extends State<XmpStructArrayCard> {
|
|
||||||
late int _index;
|
|
||||||
|
|
||||||
List<Map<String, String>> get structs => widget.structs;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_index = structs.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
void setIndex(int index) {
|
|
||||||
index = index.clamp(0, structs.length - 1);
|
|
||||||
if (_index != index) {
|
|
||||||
_index = index;
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
color: Themes.thirdLayerColor(context),
|
|
||||||
margin: XmpStructCard.cardMargin,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 8, top: 8, right: 8),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: HighlightTitle(
|
|
||||||
title: '${widget.title} ${_index + 1}',
|
|
||||||
selectable: true,
|
|
||||||
showHighlight: false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
icon: const Icon(AIcons.previous),
|
|
||||||
onPressed: _index > 0 ? () => setIndex(_index - 1) : null,
|
|
||||||
tooltip: context.l10n.previousTooltip,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
icon: const Icon(AIcons.next),
|
|
||||||
onPressed: _index < structs.length - 1 ? () => setIndex(_index + 1) : null,
|
|
||||||
tooltip: context.l10n.nextTooltip,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MultiCrossFader(
|
|
||||||
duration: Durations.xmpStructArrayCardTransition,
|
|
||||||
sizeCurve: Curves.easeOutBack,
|
|
||||||
alignment: AlignmentDirectional.topStart,
|
|
||||||
child: Padding(
|
|
||||||
// add padding at this level (instead of the column level)
|
|
||||||
// so that the crossfader can animate the content size
|
|
||||||
// without clipping the text
|
|
||||||
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
|
||||||
child: InfoRowGroup(
|
|
||||||
info: structs[_index].map((key, value) => MapEntry(XmpProp.formatKey(key), value)),
|
|
||||||
maxValueLength: Constants.infoGroupMaxValueLength,
|
|
||||||
spanBuilders: widget.linkifier?.call(_index + 1),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class XmpStructCard extends StatelessWidget {
|
|
||||||
final String title;
|
|
||||||
final Map<String, String> struct;
|
|
||||||
final Map<String, InfoValueSpanBuilder> Function()? linkifier;
|
|
||||||
|
|
||||||
static const cardMargin = EdgeInsets.symmetric(vertical: 8, horizontal: 0);
|
|
||||||
|
|
||||||
const XmpStructCard({
|
|
||||||
super.key,
|
|
||||||
required this.title,
|
|
||||||
required this.struct,
|
|
||||||
this.linkifier,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Card(
|
|
||||||
color: Themes.thirdLayerColor(context),
|
|
||||||
margin: cardMargin,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
HighlightTitle(
|
|
||||||
title: title,
|
|
||||||
selectable: true,
|
|
||||||
showHighlight: false,
|
|
||||||
),
|
|
||||||
InfoRowGroup(
|
|
||||||
info: struct.map((key, value) => MapEntry(XmpProp.formatKey(key), value)),
|
|
||||||
maxValueLength: Constants.infoGroupMaxValueLength,
|
|
||||||
spanBuilders: linkifier?.call(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue