diff --git a/lib/utils/brand_colors.dart b/lib/utils/brand_colors.dart new file mode 100644 index 000000000..1e5c99a91 --- /dev/null +++ b/lib/utils/brand_colors.dart @@ -0,0 +1,21 @@ +import 'package:flutter/painting.dart'; + +class BrandColors { + static const Color adobeIllustrator = Color(0xFFFF9B00); + static const Color adobePhotoshop = Color(0xFF2DAAFF); + static const Color android = Color(0xFF3DDC84); + static const Color flutter = Color(0xFF47D1FD); + + static Color get(String text) { + if (text != null) { + switch (text.toLowerCase()) { + case 'illustrator': + return adobeIllustrator; + case 'photoshop': + case 'lightroom': + return adobePhotoshop; + } + } + return null; + } +} diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 1d20ed1e5..9c8682314 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -3,9 +3,6 @@ import 'package:flutter/painting.dart'; import 'package:tuple/tuple.dart'; class Constants { - static const Color androidGreen = Color(0xFF3DDC84); - static const Color flutterBlue = Color(0xFF47D1FD); - // as of Flutter v1.22.3, overflowing `Text` miscalculates height and some text (e.g. 'Å') is clipped // so we give it a `strutStyle` with a slightly larger height static const overflowStrutStyle = StrutStyle(height: 1.3); diff --git a/lib/utils/xmp.dart b/lib/utils/xmp.dart new file mode 100644 index 000000000..53e7c962a --- /dev/null +++ b/lib/utils/xmp.dart @@ -0,0 +1,27 @@ +class XMP { + static const namespaceSeparator = ':'; + static const structFieldSeparator = '/'; + + static const Map namespaces = { + 'Camera': 'Camera', + 'crs': 'Camera Raw Settings', + 'dc': 'Dublin Core', + 'exif': 'Exif', + 'GIMP': 'GIMP', + 'illustrator': 'Illustrator', + 'Iptc4xmpCore': 'IPTC Core', + 'lr': 'Lightroom', + 'MicrosoftPhoto': 'Microsoft Photo', + 'pdf': 'PDF', + 'pdfx': 'PDF/X', + 'photomechanic': 'Photo Mechanic', + 'photoshop': 'Photoshop', + 'tiff': 'TIFF', + 'xmp': 'Basic', + 'xmpBJ': 'Basic Job Ticket', + 'xmpDM': 'Dynamic Media', + 'xmpMM': 'Media Management', + 'xmpRights': 'Rights Management', + 'xmpTPg': 'Paged-Text', + }; +} diff --git a/lib/widgets/about/licenses.dart b/lib/widgets/about/licenses.dart index f56456504..c47615cd1 100644 --- a/lib/widgets/about/licenses.dart +++ b/lib/widgets/about/licenses.dart @@ -1,3 +1,4 @@ +import 'package:aves/utils/brand_colors.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/aves_expansion_tile.dart'; import 'package:aves/widgets/common/icons.dart'; @@ -51,13 +52,13 @@ class _LicensesState extends State { SizedBox(height: 16), AvesExpansionTile( title: 'Android Libraries', - color: Constants.androidGreen, + color: BrandColors.android, expandedNotifier: _expandedNotifier, children: _platform.map((package) => LicenseRow(package)).toList(), ), AvesExpansionTile( title: 'Flutter Packages', - color: Constants.flutterBlue, + color: BrandColors.flutter, expandedNotifier: _expandedNotifier, children: _flutter.map((package) => LicenseRow(package)).toList(), ), diff --git a/lib/widgets/fullscreen/info/info_page.dart b/lib/widgets/fullscreen/info/info_page.dart index 9e9e4a6a1..42858849b 100644 --- a/lib/widgets/fullscreen/info/info_page.dart +++ b/lib/widgets/fullscreen/info/info_page.dart @@ -6,7 +6,7 @@ import 'package:aves/widgets/common/data_providers/media_query_data_provider.dar import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/fullscreen/info/basic_section.dart'; import 'package:aves/widgets/fullscreen/info/location_section.dart'; -import 'package:aves/widgets/fullscreen/info/metadata_section.dart'; +import 'package:aves/widgets/fullscreen/info/metadata/metadata_section.dart'; import 'package:aves/widgets/fullscreen/info/notifications.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/fullscreen/info/metadata_section.dart b/lib/widgets/fullscreen/info/metadata/metadata_section.dart similarity index 81% rename from lib/widgets/fullscreen/info/metadata_section.dart rename to lib/widgets/fullscreen/info/metadata/metadata_section.dart index 1ef58eeaa..b3f558e12 100644 --- a/lib/widgets/fullscreen/info/metadata_section.dart +++ b/lib/widgets/fullscreen/info/metadata/metadata_section.dart @@ -2,14 +2,15 @@ import 'dart:collection'; import 'package:aves/model/image_entry.dart'; import 'package:aves/services/metadata_service.dart'; +import 'package:aves/utils/brand_colors.dart'; import 'package:aves/utils/color_utils.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/common/aves_expansion_tile.dart'; -import 'package:aves/widgets/common/highlight_title.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/fullscreen/info/common.dart'; -import 'package:aves/widgets/fullscreen/info/metadata_thumbnail.dart'; +import 'package:aves/widgets/fullscreen/info/metadata/metadata_thumbnail.dart'; +import 'package:aves/widgets/fullscreen/info/metadata/xmp_tile.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -121,12 +122,17 @@ class _MetadataSectionSliverState extends State with Auto } Widget _buildDirTile(String title, _MetadataDirectory dir) { - if (dir.name == xmpDirectory) { - return _buildXmpDirTile(dir); + final dirName = dir.name; + if (dirName == xmpDirectory) { + return XmpDirTile( + entry: entry, + tags: dir.tags, + expandedNotifier: _expandedDirectoryNotifier, + ); } Widget thumbnail; final prefixChildren = []; - switch (dir.name) { + switch (dirName) { case exifThumbnailDirectory: thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.exif, entry: entry); break; @@ -150,7 +156,7 @@ class _MetadataSectionSliverState extends State with Auto return AvesExpansionTile( title: title, - color: stringToColor(dir.name), + color: BrandColors.get(dirName) ?? stringToColor(dirName), expandedNotifier: _expandedDirectoryNotifier, children: [ if (prefixChildren.isNotEmpty) Wrap(children: prefixChildren), @@ -163,43 +169,6 @@ class _MetadataSectionSliverState extends State with Auto ); } - Widget _buildXmpDirTile(_MetadataDirectory dir) { - final thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.xmp, entry: entry); - final byNamespace = SplayTreeMap.of( - groupBy, String>(dir.tags.entries, (kv) { - final fullKey = kv.key; - final i = fullKey.indexOf(':'); - if (i == -1) return ''; - return fullKey.substring(0, i); - }), - compareAsciiUpperCase, - ); - return AvesExpansionTile( - title: dir.name, - expandedNotifier: _expandedDirectoryNotifier, - children: [ - if (thumbnail != null) thumbnail, - Padding( - padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: byNamespace.entries.expand((kv) { - final ns = kv.key; - final hasNamespace = ns.isNotEmpty; - final i = hasNamespace ? ns.length + 1 : 0; - final entries = kv.value.map((kv) => MapEntry(kv.key.substring(i), kv.value)).toList(); - entries.sort((a, b) => compareAsciiUpperCaseNatural(a.key, b.key)); - return [ - if (hasNamespace) HighlightTitle(ns), - InfoRowGroup(Map.fromEntries(entries), maxValueLength: Constants.infoGroupMaxValueLength), - ]; - }).toList(), - ), - ), - ], - ); - } - void _onMetadataChanged() { _loadedMetadataUri.value = null; _metadata = {}; @@ -223,7 +192,7 @@ class _MetadataSectionSliverState extends State with Auto final rawTags = dirKV.value as Map ?? {}; final tags = SplayTreeMap.of(Map.fromEntries(rawTags.entries.map((tagKV) { - final value = tagKV.value as String ?? ''; + final value = (tagKV.value as String ?? '').trim(); if (value.isEmpty) return null; final tagName = tagKV.key as String ?? ''; return MapEntry(tagName, value); diff --git a/lib/widgets/fullscreen/info/metadata_thumbnail.dart b/lib/widgets/fullscreen/info/metadata/metadata_thumbnail.dart similarity index 100% rename from lib/widgets/fullscreen/info/metadata_thumbnail.dart rename to lib/widgets/fullscreen/info/metadata/metadata_thumbnail.dart diff --git a/lib/widgets/fullscreen/info/metadata/xmp_tile.dart b/lib/widgets/fullscreen/info/metadata/xmp_tile.dart new file mode 100644 index 000000000..f7a7175ee --- /dev/null +++ b/lib/widgets/fullscreen/info/metadata/xmp_tile.dart @@ -0,0 +1,78 @@ +import 'dart:collection'; + +import 'package:aves/model/image_entry.dart'; +import 'package:aves/utils/brand_colors.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:aves/utils/xmp.dart'; +import 'package:aves/widgets/common/aves_expansion_tile.dart'; +import 'package:aves/widgets/common/highlight_title.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'; + +class XmpDirTile extends StatelessWidget { + final ImageEntry entry; + final SplayTreeMap tags; + final ValueNotifier expandedNotifier; + + const XmpDirTile({ + @required this.entry, + @required this.tags, + @required this.expandedNotifier, + }); + + @override + Widget build(BuildContext context) { + final thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.xmp, entry: entry); + final sections = SplayTreeMap.of( + groupBy, String>(tags.entries, (kv) { + final fullKey = kv.key; + final i = fullKey.indexOf(XMP.namespaceSeparator); + if (i == -1) return ''; + final namespace = fullKey.substring(0, i); + return XMP.namespaces[namespace] ?? namespace; + }), + compareAsciiUpperCase, + ); + return AvesExpansionTile( + title: 'XMP', + expandedNotifier: 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((sectionEntry) { + final title = sectionEntry.key; + + final entries = sectionEntry.value.map((kv) { + final key = kv.key.splitMapJoin(XMP.structFieldSeparator, onNonMatch: (s) { + // strip namespace + final key = s.split(XMP.namespaceSeparator).last; + // uppercase first letter + return key.replaceFirstMapped(RegExp('.'), (m) => m.group(0).toUpperCase()); + }); + return MapEntry(key, kv.value); + }).toList() + ..sort((a, b) => compareAsciiUpperCaseNatural(a.key, b.key)); + return [ + if (title.isNotEmpty) + Padding( + padding: EdgeInsets.only(top: 8), + child: HighlightTitle( + title, + color: BrandColors.get(title), + ), + ), + InfoRowGroup(Map.fromEntries(entries), maxValueLength: Constants.infoGroupMaxValueLength), + ]; + }).toList(), + ), + ), + ], + ); + } +}