Info: show metadata from SVG
This commit is contained in:
parent
d40f32b11b
commit
6d9b6b4484
10 changed files with 128 additions and 31 deletions
|
@ -293,6 +293,12 @@ class Constants {
|
|||
licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher/LICENSE',
|
||||
sourceUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher',
|
||||
),
|
||||
Dependency(
|
||||
name: 'XML',
|
||||
license: 'MIT',
|
||||
licenseUrl: 'https://github.com/renggli/dart-xml/blob/master/LICENSE',
|
||||
sourceUrl: 'https://github.com/renggli/dart-xml',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
9
lib/utils/string_utils.dart
Normal file
9
lib/utils/string_utils.dart
Normal file
|
@ -0,0 +1,9 @@
|
|||
extension ExtraString on String {
|
||||
static final _sentenceCaseStep1 = RegExp(r'([A-Z][a-z]|\[)');
|
||||
static final _sentenceCaseStep2 = RegExp(r'([a-z])([A-Z])');
|
||||
|
||||
String toSentenceCase() {
|
||||
var s = replaceFirstMapped(RegExp('.'), (m) => m.group(0).toUpperCase());
|
||||
return s.replaceAllMapped(_sentenceCaseStep1, (m) => ' ${m.group(1)}').replaceAllMapped(_sentenceCaseStep2, (m) => '${m.group(1)} ${m.group(2)}').trim();
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:aves/image_providers/uri_image_provider.dart';
|
||||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
|
@ -202,7 +204,9 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
|||
context,
|
||||
MaterialPageRoute(
|
||||
settings: RouteSettings(name: SourceViewerPage.routeName),
|
||||
builder: (context) => SourceViewerPage(entry: entry),
|
||||
builder: (context) => SourceViewerPage(
|
||||
loader: () => ImageFileService.getImage(entry.uri, entry.mimeType, 0, false).then(utf8.decode),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import 'package:aves/utils/constants.dart';
|
|||
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/common.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/metadata/metadata_thumbnail.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/metadata/svg_tile.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/metadata/xmp_tile.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
@ -135,6 +136,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
|||
expandedNotifier: _expandedDirectoryNotifier,
|
||||
);
|
||||
}
|
||||
|
||||
Widget thumbnail;
|
||||
final prefixChildren = <Widget>[];
|
||||
switch (dirName) {
|
||||
|
@ -168,7 +170,11 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
|||
if (thumbnail != null) thumbnail,
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||
child: InfoRowGroup(dir.tags, maxValueLength: Constants.infoGroupMaxValueLength),
|
||||
child: InfoRowGroup(
|
||||
dir.tags,
|
||||
maxValueLength: Constants.infoGroupMaxValueLength,
|
||||
linkHandlers: dirName == SvgMetadata.directory ? SvgMetadata.getLinkHandlers(dir.tags) : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -184,7 +190,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
|||
if (entry == null) return;
|
||||
if (_loadedMetadataUri.value == entry.uri) return;
|
||||
if (isVisible) {
|
||||
final rawMetadata = await MetadataService.getAllMetadata(entry) ?? {};
|
||||
final rawMetadata = await (entry.isSvg ? SvgMetadata.getAllMetadata(entry) : MetadataService.getAllMetadata(entry)) ?? {};
|
||||
final directories = rawMetadata.entries.map((dirKV) {
|
||||
var directoryName = dirKV.key as String ?? '';
|
||||
|
||||
|
|
72
lib/widgets/fullscreen/info/metadata/svg_tile.dart
Normal file
72
lib/widgets/fullscreen/info/metadata/svg_tile.dart
Normal file
|
@ -0,0 +1,72 @@
|
|||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
import 'package:aves/utils/string_utils.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/common.dart';
|
||||
import 'package:aves/widgets/fullscreen/source_viewer_page.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
class SvgMetadata {
|
||||
static const directory = 'SVG';
|
||||
|
||||
static const _attributes = ['x', 'y', 'width', 'height', 'preserveAspectRatio', 'viewBox'];
|
||||
static const _textElements = ['title', 'desc'];
|
||||
static const _metadataElement = 'metadata';
|
||||
|
||||
static Future<Map<String, Map<String, String>>> getAllMetadata(ImageEntry entry) async {
|
||||
try {
|
||||
final result = <String, String>{};
|
||||
final data = await ImageFileService.getImage(entry.uri, entry.mimeType, 0, false);
|
||||
|
||||
final document = XmlDocument.parse(utf8.decode(data));
|
||||
final root = document.rootElement;
|
||||
|
||||
final metadata = root.getElement(_metadataElement);
|
||||
result.addEntries([
|
||||
...root.attributes.where((a) => _attributes.contains(a.name.qualified)).map((a) => MapEntry(_formatKey(a.name.qualified), a.value)),
|
||||
..._textElements.map((name) => MapEntry(_formatKey(name), root.getElement(name)?.text)).where((kv) => kv.value != null),
|
||||
if (metadata != null) MapEntry('Metadata', metadata.toXmlString(pretty: true)),
|
||||
]);
|
||||
|
||||
return {
|
||||
directory: result,
|
||||
};
|
||||
} catch (exception, stack) {
|
||||
debugPrint('failed to parse XML from SVG with exception=$exception\n$stack');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Map<String, InfoLinkHandler> getLinkHandlers(SplayTreeMap<String, String> tags) {
|
||||
return {
|
||||
'Metadata': InfoLinkHandler(
|
||||
linkText: 'View XML',
|
||||
onTap: (context) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: RouteSettings(name: SourceViewerPage.routeName),
|
||||
builder: (context) => SourceViewerPage(
|
||||
loader: () => SynchronousFuture(tags['Metadata']),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
static String _formatKey(String key) {
|
||||
switch (key) {
|
||||
case 'desc':
|
||||
return 'Description';
|
||||
default:
|
||||
return key.toSentenceCase();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:aves/ref/brand_colors.dart';
|
||||
import 'package:aves/ref/xmp.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/utils/string_utils.dart';
|
||||
import 'package:aves/widgets/common/identity/highlight_title.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/common.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
@ -103,23 +104,14 @@ class XmpProp {
|
|||
final String path, value;
|
||||
final String displayKey;
|
||||
|
||||
static final sentenceCaseStep1 = RegExp(r'([A-Z][a-z]|\[)');
|
||||
static final sentenceCaseStep2 = RegExp(r'([a-z])([A-Z])');
|
||||
|
||||
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
|
||||
var key = s.split(XMP.propNamespaceSeparator).last;
|
||||
|
||||
// uppercase first letter
|
||||
key = key.replaceFirstMapped(RegExp('.'), (m) => m.group(0).toUpperCase());
|
||||
|
||||
// sentence case
|
||||
return key.replaceAllMapped(sentenceCaseStep1, (m) => ' ${m.group(1)}').replaceAllMapped(sentenceCaseStep2, (m) => '${m.group(1)} ${m.group(2)}').trim();
|
||||
// strip namespace & format
|
||||
return s.split(XMP.propNamespaceSeparator).last.toSentenceCase();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
import 'package:aves/widgets/common/aves_highlight.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_highlight/themes/darcula.dart';
|
||||
|
@ -9,10 +5,10 @@ import 'package:flutter_highlight/themes/darcula.dart';
|
|||
class SourceViewerPage extends StatefulWidget {
|
||||
static const routeName = '/fullscreen/source';
|
||||
|
||||
final ImageEntry entry;
|
||||
final Future<String> Function() loader;
|
||||
|
||||
const SourceViewerPage({
|
||||
@required this.entry,
|
||||
@required this.loader,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -22,12 +18,10 @@ class SourceViewerPage extends StatefulWidget {
|
|||
class _SourceViewerPageState extends State<SourceViewerPage> {
|
||||
Future<String> _loader;
|
||||
|
||||
ImageEntry get entry => widget.entry;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loader = ImageFileService.getImage(entry.uri, entry.mimeType, 0, false).then(utf8.decode);
|
||||
_loader = widget.loader();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -40,12 +34,8 @@ class _SourceViewerPageState extends State<SourceViewerPage> {
|
|||
child: FutureBuilder<String>(
|
||||
future: _loader,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Text(snapshot.error.toString());
|
||||
}
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
if (snapshot.hasError) return Text(snapshot.error.toString());
|
||||
if (!snapshot.hasData) return SizedBox.shrink();
|
||||
|
||||
final source = snapshot.data;
|
||||
final highlightView = AvesHighlightView(
|
||||
|
|
|
@ -1146,7 +1146,7 @@ packages:
|
|||
source: hosted
|
||||
version: "0.1.2"
|
||||
xml:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: xml
|
||||
url: "https://pub.dartlang.org"
|
||||
|
|
|
@ -11,7 +11,7 @@ version: 1.2.8+34
|
|||
|
||||
# dnfield/flutter_svg (as of v0.19.1):
|
||||
# - `Could not parse "currentColor" as a color`: https://github.com/dnfield/flutter_svg/issues/31
|
||||
# - no <style> support: https://github.com/dnfield/flutter_svg/issues/105
|
||||
# - `The <style> element is not implemented in this library.`: https://github.com/dnfield/flutter_svg/issues/105
|
||||
# - inconsistent % unit support: https://github.com/dnfield/flutter_svg/issues/110
|
||||
|
||||
# video_player (as of v0.10.8+2, backed by ExoPlayer):
|
||||
|
@ -87,6 +87,7 @@ dependencies:
|
|||
streams_channel:
|
||||
tuple:
|
||||
url_launcher:
|
||||
xml:
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
17
test/utils/string_utils_test.dart
Normal file
17
test/utils/string_utils_test.dart
Normal file
|
@ -0,0 +1,17 @@
|
|||
import 'package:test/test.dart';
|
||||
import 'package:aves/utils/string_utils.dart';
|
||||
|
||||
void main() {
|
||||
test('Sentence case', () {
|
||||
expect('XResolution'.toSentenceCase(), 'X Resolution');
|
||||
expect('PixelXDimension'.toSentenceCase(), 'Pixel X Dimension');
|
||||
expect('FocalPointX'.toSentenceCase(), 'Focal Point X');
|
||||
|
||||
expect('ISOSpeedRatings[1]'.toSentenceCase(), 'ISO Speed Ratings [1]');
|
||||
expect('LegacyIPTCDigest'.toSentenceCase(), 'Legacy IPTC Digest');
|
||||
expect('DocumentID'.toSentenceCase(), 'Document ID');
|
||||
|
||||
expect('H'.toSentenceCase(), 'H');
|
||||
expect('LW[1]'.toSentenceCase(), 'LW [1]');
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue