Merge remote-tracking branch 'origin/main' into auth-fixes
This commit is contained in:
1561
mobile/lib/generated/intl/messages_pl.dart
generated
1561
mobile/lib/generated/intl/messages_pl.dart
generated
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ import "package:photos/models/location/location.dart";
|
||||
class FFProbeProps {
|
||||
Map<String, dynamic>? prodData;
|
||||
Location? location;
|
||||
DateTime? creationTime;
|
||||
DateTime? creationTimeUTC;
|
||||
String? bitrate;
|
||||
String? majorBrand;
|
||||
String? fps;
|
||||
@@ -45,7 +45,7 @@ class FFProbeProps {
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
static fromJson(Map<dynamic, dynamic>? json) {
|
||||
static parseData(Map<dynamic, dynamic>? json) {
|
||||
final Map<String, dynamic> parsedData = {};
|
||||
final FFProbeProps result = FFProbeProps();
|
||||
|
||||
@@ -76,6 +76,7 @@ class FFProbeProps {
|
||||
break;
|
||||
case FFProbeKeys.creationTime:
|
||||
parsedData[stringKey] = _formatDate(json[key] ?? "");
|
||||
result.creationTimeUTC = _getUTCDateTime(json[key] ?? "");
|
||||
break;
|
||||
case FFProbeKeys.durationMicros:
|
||||
parsedData[stringKey] = formatPreciseDuration(
|
||||
@@ -112,7 +113,20 @@ class FFProbeProps {
|
||||
}
|
||||
// iterate through the streams
|
||||
final List<dynamic> streams = json["streams"];
|
||||
final List<dynamic> newStreams = [];
|
||||
final Map<String, dynamic> metadata = {};
|
||||
for (final stream in streams) {
|
||||
if (stream['type'] == 'metadata') {
|
||||
for (final key in stream.keys) {
|
||||
if (key == FFProbeKeys.frameCount && stream[key]?.toString() == "1") {
|
||||
continue;
|
||||
}
|
||||
metadata[key] = stream[key];
|
||||
}
|
||||
metadata.remove(FFProbeKeys.index);
|
||||
} else {
|
||||
newStreams.add(stream);
|
||||
}
|
||||
for (final key in stream.keys) {
|
||||
if (key == FFProbeKeys.rFrameRate) {
|
||||
result.fps = _formatFPS(stream[key]);
|
||||
@@ -126,6 +140,10 @@ class FFProbeProps {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (metadata.isNotEmpty) {
|
||||
newStreams.add(metadata);
|
||||
}
|
||||
parsedData["streams"] = newStreams;
|
||||
result.prodData = parsedData;
|
||||
return result;
|
||||
}
|
||||
@@ -171,6 +189,16 @@ class FFProbeProps {
|
||||
return formatDateTime(newDate, 'en_US', false);
|
||||
}
|
||||
|
||||
static DateTime? _getUTCDateTime(String value) {
|
||||
final dateInUtc = DateTime.tryParse(value);
|
||||
if (dateInUtc == null) return null;
|
||||
final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
|
||||
if (dateInUtc == epoch) return null;
|
||||
return DateTime.fromMicrosecondsSinceEpoch(
|
||||
dateInUtc.microsecondsSinceEpoch,
|
||||
);
|
||||
}
|
||||
|
||||
// input example: '00:00:05.408000000' or '5.408000'
|
||||
static Duration? _parseDuration(String? value) {
|
||||
if (value == null) return null;
|
||||
|
||||
@@ -3,7 +3,6 @@ import "dart:developer";
|
||||
import "dart:io";
|
||||
|
||||
import "package:exif/exif.dart";
|
||||
import "package:ffmpeg_kit_flutter_min/ffprobe_kit.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:logging/logging.dart";
|
||||
@@ -15,6 +14,7 @@ import "package:photos/models/ffmpeg/ffprobe_props.dart";
|
||||
import "package:photos/models/file/extensions/file_props.dart";
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import 'package:photos/models/file/file_type.dart';
|
||||
import "package:photos/models/location/location.dart";
|
||||
import "package:photos/models/metadata/file_magic.dart";
|
||||
import "package:photos/service_locator.dart";
|
||||
import "package:photos/services/file_magic_service.dart";
|
||||
@@ -33,7 +33,6 @@ import "package:photos/ui/viewer/file_details/file_properties_item_widget.dart";
|
||||
import "package:photos/ui/viewer/file_details/location_tags_widget.dart";
|
||||
import "package:photos/ui/viewer/file_details/video_exif_item.dart";
|
||||
import "package:photos/utils/exif_util.dart";
|
||||
import "package:photos/utils/ffprobe_util.dart";
|
||||
import "package:photos/utils/file_util.dart";
|
||||
import "package:photos/utils/local_settings.dart";
|
||||
|
||||
@@ -89,7 +88,15 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
||||
|
||||
_exifNotifier.addListener(() {
|
||||
if (_exifNotifier.value != null && !widget.file.hasLocation) {
|
||||
_updateLocationFromExif(_exifNotifier.value!).ignore();
|
||||
_updateLocationFromExif(locationFromExif(_exifNotifier.value!))
|
||||
.ignore();
|
||||
}
|
||||
});
|
||||
_videoMetadataNotifier.addListener(() {
|
||||
if (_videoMetadataNotifier.value?.location != null &&
|
||||
!widget.file.hasLocation) {
|
||||
_updateLocationFromExif(_videoMetadataNotifier.value?.location)
|
||||
.ignore();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -117,21 +124,11 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
||||
Future<void> getMediaInfo() async {
|
||||
final File? originFile = await getFile(widget.file, isOrigin: true);
|
||||
if (originFile == null) return;
|
||||
final session = await FFprobeKit.getMediaInformation(originFile.path);
|
||||
final mediaInfo = session.getMediaInformation();
|
||||
if (mediaInfo == null) {
|
||||
final failStackTrace = await session.getFailStackTrace();
|
||||
final output = await session.getOutput();
|
||||
_logger.severe(
|
||||
'failed to get video metadata failStackTrace=$failStackTrace, output=$output',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final properties = await FFProbeUtil.getProperties(mediaInfo);
|
||||
final properties = await getVideoPropsAsync(originFile);
|
||||
_videoMetadataNotifier.value = properties;
|
||||
if (kDebugMode) {
|
||||
log("videoCustomProps ${properties.toString()}");
|
||||
log("PropData ${properties.prodData.toString()}");
|
||||
log("PropData ${properties?.prodData.toString()}");
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
@@ -343,14 +340,13 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
||||
//This code is for updating the location of files in which location data is
|
||||
//missing and the EXIF has location data. This is only happens for a
|
||||
//certain specific minority of devices.
|
||||
Future<void> _updateLocationFromExif(Map<String, IfdTag> exif) async {
|
||||
Future<void> _updateLocationFromExif(Location? locationDataFromExif) async {
|
||||
// If the file is not uploaded or the file is not owned by the current user
|
||||
// then we don't need to update the location.
|
||||
if (!widget.file.isUploaded || widget.file.ownerID! != _currentUserID) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final locationDataFromExif = locationFromExif(exif);
|
||||
if (locationDataFromExif?.latitude != null &&
|
||||
locationDataFromExif?.longitude != null) {
|
||||
widget.file.location = locationDataFromExif;
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import "dart:io";
|
||||
|
||||
import "package:flutter/foundation.dart";
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import 'package:photos/models/file/file_type.dart';
|
||||
import "package:photos/ui/viewer/file/video_widget.dart";
|
||||
import "package:photos/ui/viewer/file/video_widget_new.dart";
|
||||
import "package:photos/ui/viewer/file/zoomable_live_image_new.dart";
|
||||
|
||||
class FileWidget extends StatelessWidget {
|
||||
@@ -38,7 +42,16 @@ class FileWidget extends StatelessWidget {
|
||||
key: key ?? ValueKey(fileKey),
|
||||
);
|
||||
} else if (file.fileType == FileType.video) {
|
||||
return VideoWidget(
|
||||
// use old video widget on iOS simulator as the new one crashes while
|
||||
// playing certain videos on iOS simulator
|
||||
if (kDebugMode && Platform.isIOS) {
|
||||
return VideoWidget(
|
||||
file,
|
||||
tagPrefix: tagPrefix,
|
||||
playbackCallback: playbackCallback,
|
||||
);
|
||||
}
|
||||
return VideoWidgetNew(
|
||||
file,
|
||||
tagPrefix: tagPrefix,
|
||||
playbackCallback: playbackCallback,
|
||||
|
||||
@@ -25,12 +25,17 @@ class VideoExifDialog extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildSection(BuildContext context, String title, Widget content) {
|
||||
return ExpansionTile(
|
||||
initiallyExpanded: true,
|
||||
title: Text(title, style: getEnteTextTheme(context).largeFaint),
|
||||
childrenPadding: const EdgeInsets.symmetric(vertical: 2),
|
||||
tilePadding: const EdgeInsets.symmetric(vertical: 4),
|
||||
children: [content],
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
|
||||
child: ExpansionTile(
|
||||
initiallyExpanded: false,
|
||||
title: Text(title, style: getEnteTextTheme(context).largeFaint),
|
||||
childrenPadding: EdgeInsets.zero, // Remove padding around children
|
||||
tilePadding: EdgeInsets.zero,
|
||||
collapsedShape: const Border(), // Remove border when collapsed
|
||||
shape: const Border(),
|
||||
children: [content],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,6 +66,9 @@ class VideoExifDialog extends StatelessWidget {
|
||||
|
||||
for (final key in stream.keys) {
|
||||
final dynamic value = stream[key];
|
||||
if (value is List) {
|
||||
continue;
|
||||
}
|
||||
// print type of value
|
||||
if (value is int ||
|
||||
value is double ||
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import "dart:async";
|
||||
import "dart:developer";
|
||||
import "dart:io";
|
||||
|
||||
import "package:computer/computer.dart";
|
||||
import 'package:exif/exif.dart';
|
||||
import "package:ffmpeg_kit_flutter_min/ffprobe_kit.dart";
|
||||
import "package:ffmpeg_kit_flutter_min/media_information.dart";
|
||||
import "package:ffmpeg_kit_flutter_min/media_information_session.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import "package:photos/models/ffmpeg/ffprobe_props.dart";
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import "package:photos/models/location/location.dart";
|
||||
import "package:photos/services/location_service.dart";
|
||||
import "package:photos/utils/ffprobe_util.dart";
|
||||
import 'package:photos/utils/file_util.dart';
|
||||
|
||||
const kDateTimeOriginal = "EXIF DateTimeOriginal";
|
||||
@@ -48,6 +56,52 @@ Future<Map<String, IfdTag>?> getExifFromSourceFile(File originFile) async {
|
||||
}
|
||||
}
|
||||
|
||||
Future<FFProbeProps?> getVideoPropsAsync(File originalFile) async {
|
||||
try {
|
||||
final Map<int, StringBuffer> logs = {};
|
||||
final completer = Completer<MediaInformation?>();
|
||||
|
||||
final session = await FFprobeKit.getMediaInformationAsync(
|
||||
originalFile.path,
|
||||
(MediaInformationSession session) async {
|
||||
// This callback is called when the session is complete
|
||||
final mediaInfo = session.getMediaInformation();
|
||||
if (mediaInfo == null) {
|
||||
_logger.warning("Failed to get video metadata");
|
||||
final failStackTrace = await session.getFailStackTrace();
|
||||
final output = await session.getOutput();
|
||||
_logger.warning(
|
||||
'Failed to get video metadata. failStackTrace=$failStackTrace, output=$output',
|
||||
);
|
||||
}
|
||||
completer.complete(mediaInfo);
|
||||
},
|
||||
(log) {
|
||||
// put log messages into a map
|
||||
logs.putIfAbsent(log.getSessionId(), () => StringBuffer());
|
||||
logs[log.getSessionId()]!.write(log.getMessage());
|
||||
},
|
||||
);
|
||||
|
||||
// Wait for the session to complete
|
||||
await session.getReturnCode();
|
||||
final mediaInfo = await completer.future;
|
||||
if (kDebugMode) {
|
||||
logs.forEach((key, value) {
|
||||
log("log for session $key: $value", name: "FFprobeKit");
|
||||
});
|
||||
}
|
||||
if (mediaInfo == null) {
|
||||
return null;
|
||||
}
|
||||
final properties = await FFProbeUtil.getProperties(mediaInfo);
|
||||
return properties;
|
||||
} catch (e, s) {
|
||||
_logger.severe("Failed to getVideoProps", e, s);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<DateTime?> getCreationTimeFromEXIF(
|
||||
File? file,
|
||||
Map<String, IfdTag>? exifData,
|
||||
|
||||
@@ -17,7 +17,7 @@ class FFProbeUtil {
|
||||
final properties = await getMetadata(mediaInformation);
|
||||
|
||||
try {
|
||||
return FFProbeProps.fromJson(properties);
|
||||
return FFProbeProps.parseData(properties);
|
||||
} catch (e, stackTrace) {
|
||||
_logger.severe(
|
||||
"Error parsing FFProbe properties: $properties",
|
||||
|
||||
@@ -15,12 +15,15 @@ import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/constants.dart';
|
||||
import 'package:photos/core/errors.dart';
|
||||
import "package:photos/models/ffmpeg/ffprobe_props.dart";
|
||||
import "package:photos/models/file/extensions/file_props.dart";
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import 'package:photos/models/file/file_type.dart';
|
||||
import "package:photos/models/location/location.dart";
|
||||
import "package:photos/models/metadata/file_magic.dart";
|
||||
import "package:photos/services/file_magic_service.dart";
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
import "package:photos/utils/exif_util.dart";
|
||||
import 'package:photos/utils/file_util.dart';
|
||||
import "package:uuid/uuid.dart";
|
||||
import 'package:video_thumbnail/video_thumbnail.dart';
|
||||
@@ -110,7 +113,7 @@ Future<MediaUploadData> _getMediaUploadDataFromAssetFile(EnteFile file) async {
|
||||
);
|
||||
}
|
||||
// h4ck to fetch location data if missing (thank you Android Q+) lazily only during uploads
|
||||
await _decorateEnteFileData(file, asset);
|
||||
await _decorateEnteFileData(file, asset, sourceFile);
|
||||
fileHash = CryptoUtil.bin2base64(await CryptoUtil.getHash(sourceFile));
|
||||
|
||||
if (file.fileType == FileType.livePhoto && Platform.isIOS) {
|
||||
@@ -266,7 +269,11 @@ void _assertFileType(AssetEntity asset, EnteFile file) {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _decorateEnteFileData(EnteFile file, AssetEntity asset) async {
|
||||
Future<void> _decorateEnteFileData(
|
||||
EnteFile file,
|
||||
AssetEntity asset,
|
||||
File sourceFile,
|
||||
) async {
|
||||
// h4ck to fetch location data if missing (thank you Android Q+) lazily only during uploads
|
||||
if (file.location == null ||
|
||||
(file.location!.latitude == 0 && file.location!.longitude == 0)) {
|
||||
@@ -274,7 +281,12 @@ Future<void> _decorateEnteFileData(EnteFile file, AssetEntity asset) async {
|
||||
file.location =
|
||||
Location(latitude: latLong.latitude, longitude: latLong.longitude);
|
||||
}
|
||||
|
||||
if (!file.hasLocation && file.isVideo && Platform.isAndroid) {
|
||||
final FFProbeProps? props = await getVideoPropsAsync(sourceFile);
|
||||
if (props != null && props.location != null) {
|
||||
file.location = props.location;
|
||||
}
|
||||
}
|
||||
if (file.title == null || file.title!.isEmpty) {
|
||||
_logger.warning("Title was missing ${file.tag}");
|
||||
file.title = await asset.titleAsync;
|
||||
|
||||
@@ -12,7 +12,7 @@ description: ente photos application
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
|
||||
version: 0.9.8+908
|
||||
version: 0.9.9+909
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
|
||||
Reference in New Issue
Block a user