[mob] Display video metadata in file info
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
// Adapted from: https://github.com/deckerst/aves
|
||||
|
||||
import "dart:developer";
|
||||
|
||||
import "package:collection/collection.dart";
|
||||
import "package:intl/intl.dart";
|
||||
import "package:photos/models/ffmpeg/channel_layouts.dart";
|
||||
@@ -12,203 +10,74 @@ import "package:photos/models/ffmpeg/mp4.dart";
|
||||
import "package:photos/models/location/location.dart";
|
||||
|
||||
class FFProbeProps {
|
||||
final double? captureFps;
|
||||
final String? androidManufacturer;
|
||||
final String? androidModel;
|
||||
final String? androidVersion;
|
||||
final String? bitRate;
|
||||
final String? bitsPerRawSample;
|
||||
final String? byteCount;
|
||||
final String? channelLayout;
|
||||
final String? chromaLocation;
|
||||
final String? codecName;
|
||||
final String? codecPixelFormat;
|
||||
final int? codedHeight;
|
||||
final int? codedWidth;
|
||||
final String? colorPrimaries;
|
||||
final String? colorRange;
|
||||
final String? colorSpace;
|
||||
final String? colorTransfer;
|
||||
final String? colorProfile;
|
||||
final String? compatibleBrands;
|
||||
final String? creationTime;
|
||||
final String? displayAspectRatio;
|
||||
final DateTime? date;
|
||||
final String? duration;
|
||||
final String? durationMicros;
|
||||
final String? extraDataSize;
|
||||
final String? fieldOrder;
|
||||
final String? fpsDen;
|
||||
final int? frameCount;
|
||||
final String? handlerName;
|
||||
final bool? hasBFrames;
|
||||
final int? height;
|
||||
final String? language;
|
||||
final Location? location;
|
||||
final String? majorBrand;
|
||||
final String? mediaFormat;
|
||||
final String? mediaType;
|
||||
final String? minorVersion;
|
||||
final String? nalLengthSize;
|
||||
final String? quicktimeLocationAccuracyHorizontal;
|
||||
final int? rFrameRate;
|
||||
final String? rotate;
|
||||
final String? sampleFormat;
|
||||
final String? sampleRate;
|
||||
final String? sampleAspectRatio;
|
||||
final String? sarDen;
|
||||
final int? segmentCount;
|
||||
final String? sourceOshash;
|
||||
final String? startMicros;
|
||||
final String? startPts;
|
||||
final String? startTime;
|
||||
final String? statisticsWritingApp;
|
||||
final String? statisticsWritingDateUtc;
|
||||
final String? timeBase;
|
||||
final String? track;
|
||||
final String? vendorId;
|
||||
final int? width;
|
||||
final String? xiaomiSlowMoment;
|
||||
final Map<String, dynamic>? prodData;
|
||||
|
||||
FFProbeProps({
|
||||
required this.captureFps,
|
||||
required this.androidManufacturer,
|
||||
required this.androidModel,
|
||||
required this.androidVersion,
|
||||
required this.bitRate,
|
||||
required this.bitsPerRawSample,
|
||||
required this.byteCount,
|
||||
required this.channelLayout,
|
||||
required this.chromaLocation,
|
||||
required this.codecName,
|
||||
required this.codecPixelFormat,
|
||||
required this.codedHeight,
|
||||
required this.codedWidth,
|
||||
required this.colorPrimaries,
|
||||
required this.colorRange,
|
||||
required this.colorSpace,
|
||||
required this.colorTransfer,
|
||||
required this.colorProfile,
|
||||
required this.compatibleBrands,
|
||||
required this.creationTime,
|
||||
required this.displayAspectRatio,
|
||||
required this.date,
|
||||
required this.duration,
|
||||
required this.durationMicros,
|
||||
required this.extraDataSize,
|
||||
required this.fieldOrder,
|
||||
required this.fpsDen,
|
||||
required this.frameCount,
|
||||
required this.handlerName,
|
||||
required this.hasBFrames,
|
||||
required this.height,
|
||||
required this.language,
|
||||
required this.location,
|
||||
required this.majorBrand,
|
||||
required this.mediaFormat,
|
||||
required this.mediaType,
|
||||
required this.minorVersion,
|
||||
required this.nalLengthSize,
|
||||
required this.quicktimeLocationAccuracyHorizontal,
|
||||
required this.rFrameRate,
|
||||
required this.rotate,
|
||||
required this.sampleFormat,
|
||||
required this.sampleRate,
|
||||
required this.sampleAspectRatio,
|
||||
required this.sarDen,
|
||||
required this.segmentCount,
|
||||
required this.sourceOshash,
|
||||
required this.startMicros,
|
||||
required this.startPts,
|
||||
required this.startTime,
|
||||
required this.statisticsWritingApp,
|
||||
required this.statisticsWritingDateUtc,
|
||||
required this.timeBase,
|
||||
required this.track,
|
||||
required this.vendorId,
|
||||
required this.width,
|
||||
required this.xiaomiSlowMoment,
|
||||
required this.prodData,
|
||||
});
|
||||
|
||||
// toString() method
|
||||
@override
|
||||
String toString() {
|
||||
return 'FFProbeProps(captureFps: $captureFps, androidManufacturer: $androidManufacturer, androidModel: $androidModel, androidVersion: $androidVersion, bitRate: $bitRate, bitsPerRawSample: $bitsPerRawSample, byteCount: $byteCount, channelLayout: $channelLayout, chromaLocation: $chromaLocation, codecName: $codecName, codecPixelFormat: $codecPixelFormat, codedHeight: $codedHeight, codedWidth: $codedWidth, colorPrimaries: $colorPrimaries, colorRange: $colorRange, colorSpace: $colorSpace, colorTransfer: $colorTransfer, colorProfile: $colorProfile, compatibleBrands: $compatibleBrands, creationTime: $creationTime, displayAspectRatio: $displayAspectRatio, date: $date, duration: $duration, durationMicros: $durationMicros, extraDataSize: $extraDataSize, fieldOrder: $fieldOrder, fpsDen: $fpsDen, frameCount: $frameCount, handlerName: $handlerName, hasBFrames: $hasBFrames, height: $height, language: $language, location: $location, majorBrand: $majorBrand, mediaFormat: $mediaFormat, mediaType: $mediaType, minorVersion: $minorVersion, nalLengthSize: $nalLengthSize, quicktimeLocationAccuracyHorizontal: $quicktimeLocationAccuracyHorizontal, rFrameRate: $rFrameRate, rotate: $rotate, sampleFormat: $sampleFormat, sampleRate: $sampleRate, sampleAspectRatio: $sampleAspectRatio, sarDen: $sarDen, segmentCount: $segmentCount, sourceOshash: $sourceOshash, startMicros: $startMicros, startPts: $startPts, startTime: $startTime, statisticsWritingApp: $statisticsWritingApp, statisticsWritingDateUtc: $statisticsWritingDateUtc, timeBase: $timeBase, track: $track, vendorId: $vendorId, width: $width, xiaomiSlowMoment: $xiaomiSlowMoment)';
|
||||
final buffer = StringBuffer();
|
||||
for (final key in prodData!.keys) {
|
||||
final value = prodData![key];
|
||||
if (value != null) {
|
||||
buffer.writeln('$key: $value');
|
||||
}
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
factory FFProbeProps.fromJson(Map<dynamic, dynamic>? json) {
|
||||
return FFProbeProps(
|
||||
captureFps:
|
||||
double.tryParse(json?[FFProbeKeys.androidCaptureFramerate] ?? ""),
|
||||
androidManufacturer: json?[FFProbeKeys.androidManufacturer],
|
||||
androidModel: json?[FFProbeKeys.androidModel],
|
||||
androidVersion: json?[FFProbeKeys.androidVersion],
|
||||
bitRate: _formatMetric(
|
||||
json?[FFProbeKeys.bitrate] ?? json?[FFProbeKeys.bps],
|
||||
'b/s',
|
||||
),
|
||||
bitsPerRawSample: json?[FFProbeKeys.bitsPerRawSample],
|
||||
byteCount: _formatFilesize(json?[FFProbeKeys.byteCount]),
|
||||
channelLayout: _formatChannelLayout(json?[FFProbeKeys.channelLayout]),
|
||||
chromaLocation: json?[FFProbeKeys.chromaLocation],
|
||||
codecName: _formatCodecName(json?[FFProbeKeys.codecName]),
|
||||
codecPixelFormat:
|
||||
(json?[FFProbeKeys.codecPixelFormat] as String?)?.toUpperCase(),
|
||||
codedHeight: int.tryParse(json?[FFProbeKeys.codedHeight] ?? ""),
|
||||
codedWidth: int.tryParse(json?[FFProbeKeys.codedWidth] ?? ""),
|
||||
colorPrimaries:
|
||||
(json?[FFProbeKeys.colorPrimaries] as String?)?.toUpperCase(),
|
||||
colorRange: (json?[FFProbeKeys.colorRange] as String?)?.toUpperCase(),
|
||||
colorSpace: (json?[FFProbeKeys.colorSpace] as String?)?.toUpperCase(),
|
||||
colorTransfer:
|
||||
(json?[FFProbeKeys.colorTransfer] as String?)?.toUpperCase(),
|
||||
colorProfile: json?[FFProbeKeys.colorTransfer],
|
||||
compatibleBrands: json?[FFProbeKeys.compatibleBrands],
|
||||
creationTime: _formatDate(json?[FFProbeKeys.creationTime] ?? ""),
|
||||
displayAspectRatio: json?[FFProbeKeys.dar],
|
||||
date: DateTime.tryParse(json?[FFProbeKeys.date] ?? ""),
|
||||
duration: _formatDuration(json?[FFProbeKeys.durationMicros]),
|
||||
durationMicros: formatPreciseDuration(
|
||||
Duration(
|
||||
microseconds:
|
||||
int.tryParse(json?[FFProbeKeys.durationMicros] ?? "") ?? 0,
|
||||
),
|
||||
),
|
||||
extraDataSize: json?[FFProbeKeys.extraDataSize],
|
||||
fieldOrder: json?[FFProbeKeys.fieldOrder],
|
||||
fpsDen: json?[FFProbeKeys.fpsDen],
|
||||
frameCount: int.tryParse(json?[FFProbeKeys.frameCount] ?? ""),
|
||||
handlerName: json?[FFProbeKeys.handlerName],
|
||||
hasBFrames: json?[FFProbeKeys.hasBFrames],
|
||||
height: int.tryParse(json?[FFProbeKeys.height] ?? ""),
|
||||
language: json?[FFProbeKeys.language],
|
||||
location: _formatLocation(json?[FFProbeKeys.location]),
|
||||
majorBrand: _formatBrand(json?[FFProbeKeys.majorBrand]),
|
||||
mediaFormat: json?[FFProbeKeys.mediaFormat],
|
||||
mediaType: json?[FFProbeKeys.mediaType],
|
||||
minorVersion: json?[FFProbeKeys.minorVersion],
|
||||
nalLengthSize: json?[FFProbeKeys.nalLengthSize],
|
||||
quicktimeLocationAccuracyHorizontal:
|
||||
json?[FFProbeKeys.quicktimeLocationAccuracyHorizontal],
|
||||
rFrameRate: int.tryParse(json?[FFProbeKeys.rFrameRate] ?? ""),
|
||||
rotate: json?[FFProbeKeys.rotate],
|
||||
sampleFormat: json?[FFProbeKeys.sampleFormat],
|
||||
sampleRate: json?[FFProbeKeys.sampleRate],
|
||||
sampleAspectRatio: json?[FFProbeKeys.sar],
|
||||
sarDen: json?[FFProbeKeys.sarDen],
|
||||
segmentCount: int.tryParse(json?[FFProbeKeys.segmentCount] ?? ""),
|
||||
sourceOshash: json?[FFProbeKeys.sourceOshash],
|
||||
startMicros: json?[FFProbeKeys.startMicros],
|
||||
startPts: json?[FFProbeKeys.startPts],
|
||||
startTime: _formatDuration(json?[FFProbeKeys.startTime]),
|
||||
statisticsWritingApp: json?[FFProbeKeys.statisticsWritingApp],
|
||||
statisticsWritingDateUtc: json?[FFProbeKeys.statisticsWritingDateUtc],
|
||||
timeBase: json?[FFProbeKeys.timeBase],
|
||||
track: json?[FFProbeKeys.title],
|
||||
vendorId: json?[FFProbeKeys.vendorId],
|
||||
width: int.tryParse(json?[FFProbeKeys.width] ?? ""),
|
||||
xiaomiSlowMoment: json?[FFProbeKeys.xiaomiSlowMoment],
|
||||
);
|
||||
final Map<String, dynamic> parsedData = {};
|
||||
|
||||
for (final key in json!.keys) {
|
||||
final stringKey = key.toString();
|
||||
switch (stringKey) {
|
||||
case FFProbeKeys.bitrate:
|
||||
case FFProbeKeys.bps:
|
||||
parsedData[stringKey] = _formatMetric(json[key], 'b/s');
|
||||
break;
|
||||
case FFProbeKeys.byteCount:
|
||||
parsedData[stringKey] = _formatFilesize(json[key]);
|
||||
break;
|
||||
case FFProbeKeys.channelLayout:
|
||||
parsedData[stringKey] = _formatChannelLayout(json[key]);
|
||||
break;
|
||||
case FFProbeKeys.codecName:
|
||||
parsedData[stringKey] = _formatCodecName(json[key]);
|
||||
break;
|
||||
case FFProbeKeys.codecPixelFormat:
|
||||
case FFProbeKeys.colorPrimaries:
|
||||
case FFProbeKeys.colorRange:
|
||||
case FFProbeKeys.colorSpace:
|
||||
case FFProbeKeys.colorTransfer:
|
||||
parsedData[stringKey] = (json[key] as String?)?.toUpperCase();
|
||||
break;
|
||||
case FFProbeKeys.creationTime:
|
||||
parsedData[stringKey] = _formatDate(json[key] ?? "");
|
||||
break;
|
||||
case FFProbeKeys.durationMicros:
|
||||
parsedData[stringKey] = formatPreciseDuration(
|
||||
Duration(microseconds: int.tryParse(json[key] ?? "") ?? 0),
|
||||
);
|
||||
break;
|
||||
case FFProbeKeys.location:
|
||||
parsedData[stringKey] = _formatLocation(json[key]);
|
||||
break;
|
||||
case FFProbeKeys.majorBrand:
|
||||
parsedData[stringKey] = _formatBrand(json[key]);
|
||||
break;
|
||||
case FFProbeKeys.startTime:
|
||||
parsedData[stringKey] = _formatDuration(json[key]);
|
||||
break;
|
||||
default:
|
||||
parsedData[stringKey] = json[key];
|
||||
}
|
||||
}
|
||||
|
||||
return FFProbeProps(prodData: parsedData);
|
||||
}
|
||||
|
||||
static String _formatBrand(String value) => Mp4.brands[value] ?? value;
|
||||
|
||||
@@ -10,6 +10,7 @@ import "package:photos/core/configuration.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/people_changed_event.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/models/ffmpeg/ffprobe_props.dart";
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import 'package:photos/models/file/file_type.dart';
|
||||
import "package:photos/models/metadata/file_magic.dart";
|
||||
@@ -24,6 +25,7 @@ import "package:photos/ui/viewer/file_details/albums_item_widget.dart";
|
||||
import 'package:photos/ui/viewer/file_details/backed_up_time_item_widget.dart';
|
||||
import "package:photos/ui/viewer/file_details/creation_time_item_widget.dart";
|
||||
import 'package:photos/ui/viewer/file_details/exif_item_widgets.dart';
|
||||
import "package:photos/ui/viewer/file_details/exif_video.dart";
|
||||
import "package:photos/ui/viewer/file_details/faces_item_widget.dart";
|
||||
import "package:photos/ui/viewer/file_details/file_properties_item_widget.dart";
|
||||
import "package:photos/ui/viewer/file_details/location_tags_widget.dart";
|
||||
@@ -45,7 +47,6 @@ class FileDetailsWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
||||
final ValueNotifier<Map<String, IfdTag>?> _exifNotifier = ValueNotifier(null);
|
||||
final Map<String, dynamic> _exifData = {
|
||||
"focalLength": null,
|
||||
"fNumber": null,
|
||||
@@ -65,8 +66,11 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
||||
bool _isImage = false;
|
||||
late int _currentUserID;
|
||||
bool showExifListTile = false;
|
||||
final ValueNotifier<Map<String, IfdTag>?> _exifNotifier = ValueNotifier(null);
|
||||
final ValueNotifier<bool> hasLocationData = ValueNotifier(false);
|
||||
final Logger _logger = Logger("_FileDetailsWidgetState");
|
||||
final ValueNotifier<FFProbeProps?> _videoMetadataNotifier =
|
||||
ValueNotifier(null);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -123,11 +127,11 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
||||
return;
|
||||
}
|
||||
final properties = await FFProbeUtil.getProperties(mediaInfo);
|
||||
final parsedMetadata = await FFProbeUtil.getMetadata(mediaInfo);
|
||||
_videoMetadataNotifier.value = properties;
|
||||
|
||||
// print all the properties
|
||||
log("videoCustomProps ${properties.toString()}");
|
||||
log("videoMetadata ${parsedMetadata.toString()}");
|
||||
log("PropData ${properties.prodData.toString()}");
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
@@ -135,6 +139,7 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
||||
@override
|
||||
void dispose() {
|
||||
_exifNotifier.dispose();
|
||||
_videoMetadataNotifier.dispose();
|
||||
_peopleChangedEvent.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -192,6 +197,25 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
||||
),
|
||||
);
|
||||
|
||||
fileDetailsTiles.add(
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _videoMetadataNotifier,
|
||||
builder: (context, value, _) {
|
||||
return value != null &&
|
||||
value.prodData != null &&
|
||||
value.prodData!.isNotEmpty
|
||||
? const Column(
|
||||
children: [
|
||||
Text("show video info"),
|
||||
// VideoProbeInfo(probeData: value.prodData!),
|
||||
FileDetailsDivider(),
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
fileDetailsTiles.addAll([
|
||||
ValueListenableBuilder(
|
||||
valueListenable: hasLocationData,
|
||||
@@ -263,6 +287,22 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
||||
},
|
||||
),
|
||||
]);
|
||||
} else if (_videoMetadataNotifier.value != null) {
|
||||
fileDetailsTiles.addAll([
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _videoMetadataNotifier,
|
||||
builder: (context, value, _) {
|
||||
return (value != null && value.prodData != null)
|
||||
? Column(
|
||||
children: [
|
||||
VideoProbeInfoDetail(file, value.prodData),
|
||||
const FileDetailsDivider(),
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
if (LocalSettings.instance.isFaceIndexingEnabled) {
|
||||
|
||||
122
mobile/lib/ui/viewer/file/video_ffmpeg_info.dart
Normal file
122
mobile/lib/ui/viewer/file/video_ffmpeg_info.dart
Normal file
@@ -0,0 +1,122 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class VideoProbeInfo extends StatelessWidget {
|
||||
final Map<String, dynamic> probeData;
|
||||
|
||||
const VideoProbeInfo({Key? key, required this.probeData}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSection('General Information', _buildGeneralInfo()),
|
||||
const SizedBox(height: 8),
|
||||
_buildSection('Streams', _buildStreamsList()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSection(String title, Widget content) {
|
||||
return ExpansionTile(
|
||||
initiallyExpanded: true,
|
||||
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
children: [content],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGeneralInfo() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow('Duration', probeData, 'duration'),
|
||||
_buildInfoRow('Probe Score', probeData, 'probe_score'),
|
||||
_buildInfoRow('Number of Programs', probeData, 'nb_programs'),
|
||||
_buildInfoRow('Number of Streams', probeData, 'nb_streams'),
|
||||
_buildInfoRow('Bitrate', probeData, 'bitrate'),
|
||||
_buildInfoRow('Format', probeData, 'format'),
|
||||
_buildInfoRow('Creation Time', probeData, 'creation_time'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStreamsList() {
|
||||
final List<dynamic> streams = probeData['streams'];
|
||||
final List<Map<String, dynamic>> data = [];
|
||||
for (final stream in streams) {
|
||||
final Map<String, dynamic> streamData = {};
|
||||
|
||||
for (final key in stream.keys) {
|
||||
final dynamic value = stream[key];
|
||||
// print type of value
|
||||
if (value is int ||
|
||||
value is double ||
|
||||
value is String ||
|
||||
value is bool) {
|
||||
streamData[key] = stream[key];
|
||||
} else {
|
||||
streamData[key] = stream[key].toString();
|
||||
}
|
||||
}
|
||||
data.add(streamData);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: data.map((stream) => _buildStreamInfo(stream)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStreamInfo(Map<String, dynamic> stream) {
|
||||
return ExpansionTile(
|
||||
title: Text(
|
||||
'Stream ${stream['index']}: ${stream['codec_name']} (${stream['type']})',
|
||||
),
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: stream.entries
|
||||
.map((entry) => _buildInfoRow(entry.key, stream, entry.key))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(
|
||||
String rowName,
|
||||
Map<String, dynamic> data,
|
||||
String dataKey,
|
||||
) {
|
||||
rowName = rowName.replaceAll('_', ' ');
|
||||
rowName = rowName[0].toUpperCase() + rowName.substring(1);
|
||||
try {
|
||||
final value = data[dataKey];
|
||||
if (value == null) {
|
||||
return Container(); // Return an empty container if there's no data for the key.
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 150,
|
||||
child: Text(
|
||||
rowName,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Expanded(child: Text(value.toString())),
|
||||
],
|
||||
),
|
||||
);
|
||||
} catch (e, s) {
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
}
|
||||
90
mobile/lib/ui/viewer/file_details/exif_video.dart
Normal file
90
mobile/lib/ui/viewer/file_details/exif_video.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
import "package:flutter/material.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import "package:photos/theme/colors.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/components/info_item_widget.dart";
|
||||
import "package:photos/ui/viewer/file/video_ffmpeg_info.dart";
|
||||
import "package:photos/utils/toast_util.dart";
|
||||
|
||||
class VideoProbeInfoDetail extends StatefulWidget {
|
||||
final EnteFile file;
|
||||
final Map<String, dynamic>? exif;
|
||||
const VideoProbeInfoDetail(
|
||||
this.file,
|
||||
this.exif, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<VideoProbeInfoDetail> createState() => _VideoProbeInfoState();
|
||||
}
|
||||
|
||||
class _VideoProbeInfoState extends State<VideoProbeInfoDetail> {
|
||||
VoidCallback? _onTap;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InfoItemWidget(
|
||||
leadingIcon: Icons.text_snippet_outlined,
|
||||
title: S.of(context).exif,
|
||||
subtitleSection: _exifButton(context, widget.file, widget.exif),
|
||||
onTap: _onTap,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Widget>> _exifButton(
|
||||
BuildContext context,
|
||||
EnteFile file,
|
||||
Map<String, dynamic>? exif,
|
||||
) async {
|
||||
late final String label;
|
||||
late final VoidCallback? onTap;
|
||||
final Map<String, dynamic> data = {};
|
||||
for (final key in exif!.keys) {
|
||||
if (exif[key] != null) {
|
||||
data[key] = exif[key];
|
||||
}
|
||||
}
|
||||
if (exif == null) {
|
||||
label = S.of(context).loadingExifData;
|
||||
onTap = null;
|
||||
} else if (exif.isNotEmpty) {
|
||||
label = S.of(context).viewAllExifData;
|
||||
onTap = () => showBarModalBottomSheet(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return VideoProbeInfo(
|
||||
probeData: data,
|
||||
);
|
||||
},
|
||||
shape: const RoundedRectangleBorder(
|
||||
side: BorderSide(width: 0),
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(5),
|
||||
),
|
||||
),
|
||||
topControl: const SizedBox.shrink(),
|
||||
backgroundColor: getEnteColorScheme(context).backgroundElevated,
|
||||
barrierColor: backdropFaintDark,
|
||||
enableDrag: false,
|
||||
);
|
||||
} else {
|
||||
label = S.of(context).noExifData;
|
||||
onTap =
|
||||
() => showShortToast(context, S.of(context).thisImageHasNoExifData);
|
||||
}
|
||||
setState(() {
|
||||
_onTap = onTap;
|
||||
});
|
||||
return Future.value([
|
||||
Text(label, style: getEnteTextTheme(context).miniBoldMuted),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user