157 lines
5.8 KiB
Dart
157 lines
5.8 KiB
Dart
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);
|
|
}
|