info: google map & xmp tags
This commit is contained in:
parent
53917de437
commit
d831146135
6 changed files with 217 additions and 63 deletions
|
@ -7,6 +7,8 @@
|
||||||
android:name="io.flutter.app.FlutterApplication"
|
android:name="io.flutter.app.FlutterApplication"
|
||||||
android:label="Aves"
|
android:label="Aves"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<meta-data android:name="com.google.android.geo.API_KEY"
|
||||||
|
android:value="AIzaSyDf-1dN6JivrQGKSmxAdxERLM2egOvzGWs"/>
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
|
|
|
@ -9,13 +9,16 @@ import androidx.annotation.NonNull;
|
||||||
import com.adobe.xmp.XMPException;
|
import com.adobe.xmp.XMPException;
|
||||||
import com.adobe.xmp.XMPIterator;
|
import com.adobe.xmp.XMPIterator;
|
||||||
import com.adobe.xmp.XMPMeta;
|
import com.adobe.xmp.XMPMeta;
|
||||||
|
import com.adobe.xmp.properties.XMPProperty;
|
||||||
import com.adobe.xmp.properties.XMPPropertyInfo;
|
import com.adobe.xmp.properties.XMPPropertyInfo;
|
||||||
import com.drew.imaging.ImageMetadataReader;
|
import com.drew.imaging.ImageMetadataReader;
|
||||||
import com.drew.imaging.ImageProcessingException;
|
import com.drew.imaging.ImageProcessingException;
|
||||||
|
import com.drew.lang.GeoLocation;
|
||||||
import com.drew.metadata.Directory;
|
import com.drew.metadata.Directory;
|
||||||
import com.drew.metadata.Metadata;
|
import com.drew.metadata.Metadata;
|
||||||
import com.drew.metadata.Tag;
|
import com.drew.metadata.Tag;
|
||||||
import com.drew.metadata.exif.ExifSubIFDDirectory;
|
import com.drew.metadata.exif.ExifSubIFDDirectory;
|
||||||
|
import com.drew.metadata.exif.GpsDirectory;
|
||||||
import com.drew.metadata.xmp.XmpDirectory;
|
import com.drew.metadata.xmp.XmpDirectory;
|
||||||
|
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
|
@ -23,6 +26,7 @@ import java.io.FileNotFoundException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
import deckers.thibault.aves.utils.Constants;
|
import deckers.thibault.aves.utils.Constants;
|
||||||
import io.flutter.plugin.common.MethodCall;
|
import io.flutter.plugin.common.MethodCall;
|
||||||
|
@ -31,6 +35,9 @@ import io.flutter.plugin.common.MethodChannel;
|
||||||
public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
public static final String CHANNEL = "deckers.thibault/aves/metadata";
|
public static final String CHANNEL = "deckers.thibault/aves/metadata";
|
||||||
|
|
||||||
|
private static final String XMP_DC_SCHEMA_NS = "http://purl.org/dc/elements/1.1/";
|
||||||
|
private static final String XMP_SUBJECT_PROP_NAME = "dc:subject";
|
||||||
|
|
||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
public MetadataHandler(Context context) {
|
public MetadataHandler(Context context) {
|
||||||
|
@ -40,48 +47,21 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
@Override
|
@Override
|
||||||
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||||
switch (call.method) {
|
switch (call.method) {
|
||||||
case "getOverlayMetadata":
|
|
||||||
getOverlayMetadata(call, result);
|
|
||||||
break;
|
|
||||||
case "getAllMetadata":
|
case "getAllMetadata":
|
||||||
getAllMetadata(call, result);
|
getAllMetadata(call, result);
|
||||||
break;
|
break;
|
||||||
|
case "getCatalogMetadata":
|
||||||
|
getCatalogMetadata(call, result);
|
||||||
|
break;
|
||||||
|
case "getOverlayMetadata":
|
||||||
|
getOverlayMetadata(call, result);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
result.notImplemented();
|
result.notImplemented();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void getOverlayMetadata(MethodCall call, MethodChannel.Result result) {
|
|
||||||
String path = call.argument("path");
|
|
||||||
try (InputStream is = new FileInputStream(path)) {
|
|
||||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
|
||||||
ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
|
|
||||||
Map<String, String> metadataMap = new HashMap<>();
|
|
||||||
if (directory != null) {
|
|
||||||
if (directory.containsTag(ExifSubIFDDirectory.TAG_FNUMBER)) {
|
|
||||||
metadataMap.put("aperture", directory.getDescription(ExifSubIFDDirectory.TAG_FNUMBER));
|
|
||||||
}
|
|
||||||
if (directory.containsTag(ExifSubIFDDirectory.TAG_EXPOSURE_TIME)) {
|
|
||||||
metadataMap.put("exposureTime", directory.getString(ExifSubIFDDirectory.TAG_EXPOSURE_TIME));
|
|
||||||
}
|
|
||||||
if (directory.containsTag(ExifSubIFDDirectory.TAG_FOCAL_LENGTH)) {
|
|
||||||
metadataMap.put("focalLength", directory.getDescription(ExifSubIFDDirectory.TAG_FOCAL_LENGTH));
|
|
||||||
}
|
|
||||||
if (directory.containsTag(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT)) {
|
|
||||||
metadataMap.put("iso", "ISO" + directory.getDescription(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.success(metadataMap);
|
|
||||||
} catch (ImageProcessingException e) {
|
|
||||||
result.error("getOverlayMetadata-imageprocessing", "failed to get metadata for path=" + path + " (" + e.getMessage() + ")", null);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
result.error("getOverlayMetadata-filenotfound", "failed to get metadata for path=" + path + " (" + e.getMessage() + ")", null);
|
|
||||||
} catch (Exception e) {
|
|
||||||
result.error("getOverlayMetadata-exception", "failed to get metadata for path=" + path, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void getAllMetadata(MethodCall call, MethodChannel.Result result) {
|
private void getAllMetadata(MethodCall call, MethodChannel.Result result) {
|
||||||
String path = call.argument("path");
|
String path = call.argument("path");
|
||||||
try (InputStream is = new FileInputStream(path)) {
|
try (InputStream is = new FileInputStream(path)) {
|
||||||
|
@ -159,4 +139,84 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
result.error("getAllVideoMetadataFallback-exception", "failed to get metadata for path=" + path, e);
|
result.error("getAllVideoMetadataFallback-exception", "failed to get metadata for path=" + path, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void getCatalogMetadata(MethodCall call, MethodChannel.Result result) {
|
||||||
|
String path = call.argument("path");
|
||||||
|
try (InputStream is = new FileInputStream(path)) {
|
||||||
|
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||||
|
Map<String, Object> metadataMap = new HashMap<>();
|
||||||
|
|
||||||
|
// EXIF Sub-IFD
|
||||||
|
ExifSubIFDDirectory exifSubDir = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
|
||||||
|
if (exifSubDir != null) {
|
||||||
|
if (exifSubDir.containsTag(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL)) {
|
||||||
|
metadataMap.put("dateMillis", exifSubDir.getDate(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL, null, TimeZone.getDefault()).getTime());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GPS
|
||||||
|
GpsDirectory gpsDir = metadata.getFirstDirectoryOfType(GpsDirectory.class);
|
||||||
|
if (gpsDir != null) {
|
||||||
|
GeoLocation geoLocation = gpsDir.getGeoLocation();
|
||||||
|
if (geoLocation != null) {
|
||||||
|
metadataMap.put("latitude", geoLocation.getLatitude());
|
||||||
|
metadataMap.put("longitude", geoLocation.getLongitude());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// XMP
|
||||||
|
XmpDirectory xmpDir = metadata.getFirstDirectoryOfType(XmpDirectory.class);
|
||||||
|
if (xmpDir != null) {
|
||||||
|
XMPMeta xmpMeta = xmpDir.getXMPMeta();
|
||||||
|
try {
|
||||||
|
if (xmpMeta.doesPropertyExist(XMP_DC_SCHEMA_NS, XMP_SUBJECT_PROP_NAME)) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
int count = xmpMeta.countArrayItems(XMP_DC_SCHEMA_NS, XMP_SUBJECT_PROP_NAME);
|
||||||
|
for (int i = 1; i < count + 1; i++) {
|
||||||
|
XMPProperty item = xmpMeta.getArrayItem(XMP_DC_SCHEMA_NS, XMP_SUBJECT_PROP_NAME, i);
|
||||||
|
sb.append(" ").append(item.getValue());
|
||||||
|
}
|
||||||
|
metadataMap.put("keywords", sb.toString());
|
||||||
|
}
|
||||||
|
} catch (XMPException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.success(metadataMap);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
result.error("getCatalogMetadata-filenotfound", "failed to get metadata for path=" + path + " (" + e.getMessage() + ")", null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.error("getCatalogMetadata-exception", "failed to get metadata for path=" + path, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getOverlayMetadata(MethodCall call, MethodChannel.Result result) {
|
||||||
|
String path = call.argument("path");
|
||||||
|
try (InputStream is = new FileInputStream(path)) {
|
||||||
|
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||||
|
ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
|
||||||
|
Map<String, String> metadataMap = new HashMap<>();
|
||||||
|
if (directory != null) {
|
||||||
|
if (directory.containsTag(ExifSubIFDDirectory.TAG_FNUMBER)) {
|
||||||
|
metadataMap.put("aperture", directory.getDescription(ExifSubIFDDirectory.TAG_FNUMBER));
|
||||||
|
}
|
||||||
|
if (directory.containsTag(ExifSubIFDDirectory.TAG_EXPOSURE_TIME)) {
|
||||||
|
metadataMap.put("exposureTime", directory.getString(ExifSubIFDDirectory.TAG_EXPOSURE_TIME));
|
||||||
|
}
|
||||||
|
if (directory.containsTag(ExifSubIFDDirectory.TAG_FOCAL_LENGTH)) {
|
||||||
|
metadataMap.put("focalLength", directory.getDescription(ExifSubIFDDirectory.TAG_FOCAL_LENGTH));
|
||||||
|
}
|
||||||
|
if (directory.containsTag(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT)) {
|
||||||
|
metadataMap.put("iso", "ISO" + directory.getDescription(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.success(metadataMap);
|
||||||
|
} catch (ImageProcessingException e) {
|
||||||
|
result.error("getOverlayMetadata-imageprocessing", "failed to get metadata for path=" + path + " (" + e.getMessage() + ")", null);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
result.error("getOverlayMetadata-filenotfound", "failed to get metadata for path=" + path + " (" + e.getMessage() + ")", null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.error("getOverlayMetadata-exception", "failed to get metadata for path=" + path, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -4,20 +4,7 @@ import 'package:flutter/services.dart';
|
||||||
class MetadataService {
|
class MetadataService {
|
||||||
static const platform = const MethodChannel('deckers.thibault/aves/metadata');
|
static const platform = const MethodChannel('deckers.thibault/aves/metadata');
|
||||||
|
|
||||||
// return map with: 'aperture' 'exposureTime' 'focalLength' 'iso'
|
// return Map<Map<Key, Value>> (map of directories, each directory being a map of metadata label and value description)
|
||||||
static Future<Map> getOverlayMetadata(String path) async {
|
|
||||||
try {
|
|
||||||
final result = await platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
|
|
||||||
'path': path,
|
|
||||||
});
|
|
||||||
return result as Map;
|
|
||||||
} on PlatformException catch (e) {
|
|
||||||
debugPrint('getOverlayMetadata failed with exception=${e.message}');
|
|
||||||
}
|
|
||||||
return Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
// return Map<Map<Key, Value>>
|
|
||||||
static Future<Map> getAllMetadata(String path) async {
|
static Future<Map> getAllMetadata(String path) async {
|
||||||
try {
|
try {
|
||||||
final result = await platform.invokeMethod('getAllMetadata', <String, dynamic>{
|
final result = await platform.invokeMethod('getAllMetadata', <String, dynamic>{
|
||||||
|
@ -29,4 +16,34 @@ class MetadataService {
|
||||||
}
|
}
|
||||||
return Map();
|
return Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// return map with:
|
||||||
|
// 'dateMillis': date taken in milliseconds since Epoch (long)
|
||||||
|
// 'latitude': latitude (double)
|
||||||
|
// 'longitude': longitude (double)
|
||||||
|
// 'keywords': space separated XMP subjects (string)
|
||||||
|
static Future<Map> getCatalogMetadata(String path) async {
|
||||||
|
try {
|
||||||
|
final result = await platform.invokeMethod('getCatalogMetadata', <String, dynamic>{
|
||||||
|
'path': path,
|
||||||
|
});
|
||||||
|
return result as Map;
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
debugPrint('getCatalogMetadata failed with exception=${e.message}');
|
||||||
|
}
|
||||||
|
return Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
// return map with string descriptions for: 'aperture' 'exposureTime' 'focalLength' 'iso'
|
||||||
|
static Future<Map> getOverlayMetadata(String path) async {
|
||||||
|
try {
|
||||||
|
final result = await platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
|
||||||
|
'path': path,
|
||||||
|
});
|
||||||
|
return result as Map;
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
debugPrint('getOverlayMetadata failed with exception=${e.message}');
|
||||||
|
}
|
||||||
|
return Map();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/metadata_service.dart';
|
import 'package:aves/model/metadata_service.dart';
|
||||||
import 'package:aves/utils/file_utils.dart';
|
import 'package:aves/utils/file_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
class InfoPage extends StatefulWidget {
|
class InfoPage extends StatefulWidget {
|
||||||
|
@ -14,7 +17,7 @@ class InfoPage extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class InfoPageState extends State<InfoPage> {
|
class InfoPageState extends State<InfoPage> {
|
||||||
Future<Map> _metadataLoader;
|
Future<Map> _catalogLoader, _metadataLoader;
|
||||||
bool _scrollStartFromTop = false;
|
bool _scrollStartFromTop = false;
|
||||||
|
|
||||||
ImageEntry get entry => widget.entry;
|
ImageEntry get entry => widget.entry;
|
||||||
|
@ -32,6 +35,7 @@ class InfoPageState extends State<InfoPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
initMetadataLoader() {
|
initMetadataLoader() {
|
||||||
|
_catalogLoader = MetadataService.getCatalogMetadata(entry.path);
|
||||||
_metadataLoader = MetadataService.getAllMetadata(entry.path);
|
_metadataLoader = MetadataService.getAllMetadata(entry.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,41 +76,55 @@ class InfoPageState extends State<InfoPage> {
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: EdgeInsets.all(8.0),
|
padding: EdgeInsets.all(8.0),
|
||||||
children: [
|
children: [
|
||||||
SectionRow('File'),
|
|
||||||
InfoRow('Title', entry.title),
|
InfoRow('Title', entry.title),
|
||||||
InfoRow('Date', dateText),
|
InfoRow('Date', dateText),
|
||||||
if (entry.isVideo) InfoRow('Duration', entry.durationText),
|
if (entry.isVideo) InfoRow('Duration', entry.durationText),
|
||||||
InfoRow('Resolution', resolutionText),
|
InfoRow('Resolution', resolutionText),
|
||||||
InfoRow('Size', formatFilesize(entry.sizeBytes)),
|
InfoRow('Size', formatFilesize(entry.sizeBytes)),
|
||||||
InfoRow('Path', entry.path),
|
InfoRow('Path', entry.path),
|
||||||
SectionRow('Metadata'),
|
FutureBuilder(
|
||||||
|
future: _catalogLoader,
|
||||||
|
builder: (futureContext, AsyncSnapshot<Map> snapshot) {
|
||||||
|
if (snapshot.hasError) return Text(snapshot.error);
|
||||||
|
if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink();
|
||||||
|
final metadata = snapshot.data;
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
..._buildLocationSection(metadata['latitude'], metadata['longitude']),
|
||||||
|
..._buildTagSection(metadata['keywords']),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
FutureBuilder(
|
FutureBuilder(
|
||||||
future: _metadataLoader,
|
future: _metadataLoader,
|
||||||
builder: (futureContext, AsyncSnapshot<Map> snapshot) {
|
builder: (futureContext, AsyncSnapshot<Map> snapshot) {
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) return Text(snapshot.error);
|
||||||
return Text(snapshot.error);
|
if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink();
|
||||||
}
|
|
||||||
if (snapshot.connectionState != ConnectionState.done) {
|
|
||||||
return SizedBox.shrink();
|
|
||||||
}
|
|
||||||
final metadataMap = snapshot.data.cast<String, Map>();
|
final metadataMap = snapshot.data.cast<String, Map>();
|
||||||
final directoryNames = metadataMap.keys.toList()..sort();
|
final directoryNames = metadataMap.keys.toList()..sort();
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: directoryNames.expand(
|
children: [
|
||||||
|
SectionRow('Metadata'),
|
||||||
|
...directoryNames.expand(
|
||||||
(directoryName) {
|
(directoryName) {
|
||||||
final directory = metadataMap[directoryName];
|
final directory = metadataMap[directoryName];
|
||||||
final tagKeys = directory.keys.toList()..sort();
|
final tagKeys = directory.keys.toList()..sort();
|
||||||
return [
|
return [
|
||||||
if (directoryName.isNotEmpty) Padding(
|
if (directoryName.isNotEmpty)
|
||||||
padding: EdgeInsets.symmetric(vertical: 4.0),
|
Padding(
|
||||||
child: Text(directoryName, style: TextStyle(fontSize: 18)),
|
padding: EdgeInsets.symmetric(vertical: 4.0),
|
||||||
),
|
child: Text(directoryName, style: TextStyle(fontSize: 18)),
|
||||||
|
),
|
||||||
...tagKeys.map((tagKey) => InfoRow(tagKey, directory[tagKey])),
|
...tagKeys.map((tagKey) => InfoRow(tagKey, directory[tagKey])),
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
).toList());
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -114,6 +132,55 @@ class InfoPageState extends State<InfoPage> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildLocationSection(double latitude, double longitude) {
|
||||||
|
if (latitude == null || longitude == null) return [];
|
||||||
|
final latLng = LatLng(latitude, longitude);
|
||||||
|
return [
|
||||||
|
SectionRow('Location'),
|
||||||
|
SizedBox(
|
||||||
|
height: 200,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(16),
|
||||||
|
),
|
||||||
|
child: GoogleMap(
|
||||||
|
initialCameraPosition: CameraPosition(
|
||||||
|
target: latLng,
|
||||||
|
zoom: 12,
|
||||||
|
),
|
||||||
|
markers: [
|
||||||
|
Marker(
|
||||||
|
markerId: MarkerId(entry.path),
|
||||||
|
icon: BitmapDescriptor.defaultMarker,
|
||||||
|
position: latLng,
|
||||||
|
)
|
||||||
|
].toSet(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildTagSection(String keywords) {
|
||||||
|
if (keywords == null) return [];
|
||||||
|
return [
|
||||||
|
SectionRow('XMP Tags'),
|
||||||
|
Wrap(
|
||||||
|
children: keywords
|
||||||
|
.split(' ')
|
||||||
|
.where((word) => word.isNotEmpty)
|
||||||
|
.map((word) => Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
|
child: Chip(
|
||||||
|
backgroundColor: Colors.indigo,
|
||||||
|
label: Text(word),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SectionRow extends StatelessWidget {
|
class SectionRow extends StatelessWidget {
|
||||||
|
|
|
@ -60,6 +60,13 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
google_maps_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: google_maps_flutter
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.20+1"
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -22,6 +22,7 @@ dependencies:
|
||||||
chewie:
|
chewie:
|
||||||
collection:
|
collection:
|
||||||
flutter_sticky_header:
|
flutter_sticky_header:
|
||||||
|
google_maps_flutter:
|
||||||
intl:
|
intl:
|
||||||
photo_view:
|
photo_view:
|
||||||
screen:
|
screen:
|
||||||
|
|
Loading…
Reference in a new issue