aves/lib/widgets/fullscreen/info/metadata/xmp_tile.dart
2020-12-03 21:25:26 +09:00

164 lines
5.8 KiB
Dart

import 'dart:collection';
import 'package:aves/model/image_entry.dart';
import 'package:aves/ref/brand_colors.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/ref/xmp.dart';
import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/metadata_service.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/behaviour/routes.dart';
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
import 'package:aves/widgets/common/identity/highlight_title.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
import 'package:aves/widgets/fullscreen/info/common.dart';
import 'package:aves/widgets/fullscreen/info/metadata/metadata_thumbnail.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:pedantic/pedantic.dart';
class XmpDirTile extends StatefulWidget {
final ImageEntry entry;
final SplayTreeMap<String, String> tags;
final ValueNotifier<String> expandedNotifier;
const XmpDirTile({
@required this.entry,
@required this.tags,
@required this.expandedNotifier,
});
@override
_XmpDirTileState createState() => _XmpDirTileState();
}
class _XmpDirTileState extends State<XmpDirTile> with FeedbackMixin {
ImageEntry get entry => widget.entry;
@override
Widget build(BuildContext context) {
final thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.xmp, entry: entry);
final sections = SplayTreeMap<_XmpNamespace, List<MapEntry<String, String>>>.of(
groupBy(widget.tags.entries, (kv) {
final fullKey = kv.key;
final i = fullKey.indexOf(XMP.propNamespaceSeparator);
if (i == -1) return _XmpNamespace('');
final namespace = fullKey.substring(0, i);
return _XmpNamespace(namespace);
}),
(a, b) => compareAsciiUpperCase(a.displayTitle, b.displayTitle),
);
return AvesExpansionTile(
title: 'XMP',
expandedNotifier: widget.expandedNotifier,
children: [
if (thumbnail != null) thumbnail,
Padding(
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: sections.entries.expand((namespaceProps) {
final namespace = namespaceProps.key;
final displayNamespace = namespace.displayTitle;
final linkHandlers = <String, InfoLinkHandler>{};
final entries = namespaceProps.value.map((prop) {
final propPath = prop.key;
final displayKey = propPath.splitMapJoin(XMP.structFieldSeparator, onNonMatch: (s) {
// strip namespace
final key = s.split(XMP.propNamespaceSeparator).last;
// uppercase first letter
return key.replaceFirstMapped(RegExp('.'), (m) => m.group(0).toUpperCase());
});
var value = prop.value;
if (XMP.dataProps.contains(propPath)) {
linkHandlers.putIfAbsent(
displayKey,
() => InfoLinkHandler(linkText: 'Open', onTap: () => _openEmbeddedData(propPath)),
);
}
return MapEntry(displayKey, value);
}).toList()
..sort((a, b) => compareAsciiUpperCaseNatural(a.key, b.key));
return [
if (displayNamespace.isNotEmpty)
Padding(
padding: EdgeInsets.only(top: 8),
child: HighlightTitle(
displayNamespace,
color: BrandColors.get(displayNamespace),
selectable: true,
),
),
InfoRowGroup(
Map.fromEntries(entries),
maxValueLength: Constants.infoGroupMaxValueLength,
linkHandlers: linkHandlers,
),
];
}).toList(),
),
),
],
);
}
Future<void> _openEmbeddedData(String propPath) async {
final fields = await MetadataService.extractXmpDataProp(entry, propPath);
if (fields == null || !fields.containsKey('mimeType') || !fields.containsKey('uri')) {
showFeedback(context, 'Failed');
return;
}
final mimeType = fields['mimeType'];
final uri = fields['uri'];
if (!MimeTypes.isImage(mimeType) && !MimeTypes.isVideo(mimeType)) {
// open with another app
unawaited(AndroidAppService.open(uri, mimeType).then((success) {
if (!success) {
// fallback to sharing, so that the file can be saved somewhere
AndroidAppService.shareSingle(uri, mimeType).then((success) {
if (!success) showNoMatchingAppDialog(context);
});
}
}));
return;
}
final embedEntry = ImageEntry.fromMap(fields);
unawaited(Navigator.push(
context,
TransparentMaterialPageRoute(
settings: RouteSettings(name: SingleFullscreenPage.routeName),
pageBuilder: (c, a, sa) => SingleFullscreenPage(entry: embedEntry),
),
));
}
}
class _XmpNamespace {
final String namespace;
const _XmpNamespace(this.namespace);
String get displayTitle => XMP.namespaces[namespace] ?? namespace;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is _XmpNamespace && other.namespace == namespace;
}
@override
int get hashCode => namespace.hashCode;
@override
String toString() {
return '$runtimeType#${shortHash(this)}{namespace=$namespace}';
}
}