Преглед изворни кода

Merge branch 'main' into 30_million

Vishnu Mohandas пре 1 година
родитељ
комит
f2c05c49a3

+ 50 - 0
lib/gateways/cast_gw.dart

@@ -0,0 +1,50 @@
+import "package:dio/dio.dart";
+
+class CastGateway {
+  final Dio _enteDio;
+
+  CastGateway(this._enteDio);
+
+  Future<String?> getPublicKey(String deviceCode) async {
+    try {
+      final response = await _enteDio.get(
+        "/cast/device-info/$deviceCode",
+      );
+      return response.data["publicKey"];
+    } catch (e) {
+      if (e is DioError &&
+          e.response != null &&
+          e.response!.statusCode == 404) {
+        return null;
+      }
+      rethrow;
+    }
+  }
+
+  Future<void> publishCastPayload(
+    String code,
+    String castPayload,
+    int collectionID,
+    String castToken,
+  ) {
+    return _enteDio.post(
+      "/cast/cast-data/",
+      data: {
+        "deviceCode": code,
+        "encPayload": castPayload,
+        "collectionID": collectionID,
+        "castToken": castToken,
+      },
+    );
+  }
+
+  Future<void> revokeAllTokens() async {
+    try {
+      await _enteDio.delete(
+        "/cast/revoke-all-tokens/",
+      );
+    } catch (e) {
+      // swallow error
+    }
+  }
+}

+ 8 - 0
lib/generated/intl/messages_en.dart

@@ -380,6 +380,8 @@ class MessageLookup extends MessageLookupByLibrary {
         "cannotAddMorePhotosAfterBecomingViewer": m7,
         "cannotAddMorePhotosAfterBecomingViewer": m7,
         "cannotDeleteSharedFiles":
         "cannotDeleteSharedFiles":
             MessageLookupByLibrary.simpleMessage("Cannot delete shared files"),
             MessageLookupByLibrary.simpleMessage("Cannot delete shared files"),
+        "castInstruction": MessageLookupByLibrary.simpleMessage(
+            "Visit cast.ente.io on the device you want to pair.\n\nEnter the code below to play the album on your TV."),
         "centerPoint": MessageLookupByLibrary.simpleMessage("Center point"),
         "centerPoint": MessageLookupByLibrary.simpleMessage("Center point"),
         "changeEmail": MessageLookupByLibrary.simpleMessage("Change email"),
         "changeEmail": MessageLookupByLibrary.simpleMessage("Change email"),
         "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage(
         "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage(
@@ -552,10 +554,14 @@ class MessageLookup extends MessageLookupByLibrary {
         "details": MessageLookupByLibrary.simpleMessage("Details"),
         "details": MessageLookupByLibrary.simpleMessage("Details"),
         "devAccountChanged": MessageLookupByLibrary.simpleMessage(
         "devAccountChanged": MessageLookupByLibrary.simpleMessage(
             "The developer account we use to publish ente on App Store has changed. Because of this, you will need to login again.\n\nOur apologies for the inconvenience, but this was unavoidable."),
             "The developer account we use to publish ente on App Store has changed. Because of this, you will need to login again.\n\nOur apologies for the inconvenience, but this was unavoidable."),
+        "deviceCodeHint":
+            MessageLookupByLibrary.simpleMessage("Enter the code"),
         "deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage(
         "deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage(
             "Files added to this device album will automatically get uploaded to ente."),
             "Files added to this device album will automatically get uploaded to ente."),
         "deviceLockExplanation": MessageLookupByLibrary.simpleMessage(
         "deviceLockExplanation": MessageLookupByLibrary.simpleMessage(
             "Disable the device screen lock when ente is in the foreground and there is a backup in progress. This is normally not needed, but may help big uploads and initial imports of large libraries complete faster."),
             "Disable the device screen lock when ente is in the foreground and there is a backup in progress. This is normally not needed, but may help big uploads and initial imports of large libraries complete faster."),
+        "deviceNotFound":
+            MessageLookupByLibrary.simpleMessage("Device not found"),
         "didYouKnow": MessageLookupByLibrary.simpleMessage("Did you know?"),
         "didYouKnow": MessageLookupByLibrary.simpleMessage("Did you know?"),
         "disableAutoLock":
         "disableAutoLock":
             MessageLookupByLibrary.simpleMessage("Disable auto lock"),
             MessageLookupByLibrary.simpleMessage("Disable auto lock"),
@@ -946,6 +952,7 @@ class MessageLookup extends MessageLookupByLibrary {
             "Optional, as short as you like..."),
             "Optional, as short as you like..."),
         "orPickAnExistingOne":
         "orPickAnExistingOne":
             MessageLookupByLibrary.simpleMessage("Or pick an existing one"),
             MessageLookupByLibrary.simpleMessage("Or pick an existing one"),
+        "pair": MessageLookupByLibrary.simpleMessage("Pair"),
         "password": MessageLookupByLibrary.simpleMessage("Password"),
         "password": MessageLookupByLibrary.simpleMessage("Password"),
         "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage(
         "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage(
             "Password changed successfully"),
             "Password changed successfully"),
@@ -980,6 +987,7 @@ class MessageLookup extends MessageLookupByLibrary {
         "pickCenterPoint":
         "pickCenterPoint":
             MessageLookupByLibrary.simpleMessage("Pick center point"),
             MessageLookupByLibrary.simpleMessage("Pick center point"),
         "pinAlbum": MessageLookupByLibrary.simpleMessage("Pin album"),
         "pinAlbum": MessageLookupByLibrary.simpleMessage("Pin album"),
+        "playOnTv": MessageLookupByLibrary.simpleMessage("Play album on TV"),
         "playStoreFreeTrialValidTill": m37,
         "playStoreFreeTrialValidTill": m37,
         "playstoreSubscription":
         "playstoreSubscription":
             MessageLookupByLibrary.simpleMessage("PlayStore subscription"),
             MessageLookupByLibrary.simpleMessage("PlayStore subscription"),

+ 50 - 0
lib/generated/l10n.dart

@@ -8307,6 +8307,56 @@ class S {
       args: [],
       args: [],
     );
     );
   }
   }
+
+  /// `Play album on TV`
+  String get playOnTv {
+    return Intl.message(
+      'Play album on TV',
+      name: 'playOnTv',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Pair`
+  String get pair {
+    return Intl.message(
+      'Pair',
+      name: 'pair',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Device not found`
+  String get deviceNotFound {
+    return Intl.message(
+      'Device not found',
+      name: 'deviceNotFound',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Visit cast.ente.io on the device you want to pair.\n\nEnter the code below to play the album on your TV.`
+  String get castInstruction {
+    return Intl.message(
+      'Visit cast.ente.io on the device you want to pair.\n\nEnter the code below to play the album on your TV.',
+      name: 'castInstruction',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Enter the code`
+  String get deviceCodeHint {
+    return Intl.message(
+      'Enter the code',
+      name: 'deviceCodeHint',
+      desc: '',
+      args: [],
+    );
+  }
 }
 }
 
 
 class AppLocalizationDelegate extends LocalizationsDelegate<S> {
 class AppLocalizationDelegate extends LocalizationsDelegate<S> {

+ 6 - 1
lib/l10n/intl_en.arb

@@ -1187,5 +1187,10 @@
   "selectALocationFirst": "Select a location first",
   "selectALocationFirst": "Select a location first",
   "changeLocationOfSelectedItems": "Change location of selected items?",
   "changeLocationOfSelectedItems": "Change location of selected items?",
   "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente",
   "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente",
-  "cleanUncategorized": "Clean Uncategorized"
+  "cleanUncategorized": "Clean Uncategorized",
+  "playOnTv": "Play album on TV",
+  "pair": "Pair",
+  "deviceNotFound": "Device not found",
+  "castInstruction": "Visit cast.ente.io on the device you want to pair.\n\nEnter the code below to play the album on your TV.",
+  "deviceCodeHint": "Enter the code"
 }
 }

+ 17 - 0
lib/services/collections_service.dart

@@ -473,6 +473,23 @@ class CollectionsService {
     });
     });
   }
   }
 
 
+  String getCastData(
+    String castToken,
+    Collection collection,
+    String publicKey,
+  ) {
+    final String payload = jsonEncode({
+      "collectionID": collection.id,
+      "castToken": castToken,
+      "collectionKey": CryptoUtil.bin2base64(getCollectionKey(collection.id)),
+    });
+    final encPayload = CryptoUtil.sealSync(
+      CryptoUtil.base642bin(base64Encode(payload.codeUnits)),
+      CryptoUtil.base642bin(publicKey),
+    );
+    return CryptoUtil.bin2base64(encPayload);
+  }
+
   Future<List<User>> share(
   Future<List<User>> share(
     int collectionID,
     int collectionID,
     String email,
     String email,

+ 1 - 4
lib/services/location_service.dart

@@ -13,7 +13,6 @@ import "package:photos/models/local_entity_data.dart";
 import "package:photos/models/location/location.dart";
 import "package:photos/models/location/location.dart";
 import 'package:photos/models/location_tag/location_tag.dart';
 import 'package:photos/models/location_tag/location_tag.dart';
 import "package:photos/services/entity_service.dart";
 import "package:photos/services/entity_service.dart";
-import "package:photos/services/feature_flag_service.dart";
 import "package:photos/services/remote_assets_service.dart";
 import "package:photos/services/remote_assets_service.dart";
 import "package:shared_preferences/shared_preferences.dart";
 import "package:shared_preferences/shared_preferences.dart";
 
 
@@ -32,9 +31,7 @@ class LocationService {
 
 
   void init(SharedPreferences preferences) {
   void init(SharedPreferences preferences) {
     prefs = preferences;
     prefs = preferences;
-    if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) {
-      _loadCities();
-    }
+    _loadCities();
   }
   }
 
 
   Future<Iterable<LocalEntity<LocationTag>>> _getStoredLocationTags() async {
   Future<Iterable<LocalEntity<LocationTag>>> _getStoredLocationTags() async {

+ 52 - 35
lib/ui/map/map_screen.dart

@@ -2,6 +2,7 @@ import "dart:async";
 import "dart:isolate";
 import "dart:isolate";
 
 
 import "package:collection/collection.dart";
 import "package:collection/collection.dart";
+import "package:computer/computer.dart";
 import "package:flutter/foundation.dart";
 import "package:flutter/foundation.dart";
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_map/flutter_map.dart';
 import 'package:flutter_map/flutter_map.dart';
@@ -79,43 +80,15 @@ class _MapScreenState extends State<MapScreen> {
   }
   }
 
 
   Future<void> processFiles(List<EnteFile> files) async {
   Future<void> processFiles(List<EnteFile> files) async {
-    final List<ImageMarker> tempMarkers = [];
-    bool hasAnyLocation = false;
-    EnteFile? mostRecentFile;
-    for (var file in files) {
-      if (file.hasLocation) {
-        if (!Location.isValidRange(
-          latitude: file.location!.latitude!,
-          longitude: file.location!.longitude!,
-        )) {
-          _logger.warning(
-            'Skipping file with invalid location ${file.toString()}',
-          );
-          continue;
-        }
-        hasAnyLocation = true;
-
-        if (widget.center == null) {
-          if (mostRecentFile == null) {
-            mostRecentFile = file;
-          } else {
-            if ((mostRecentFile.creationTime ?? 0) < (file.creationTime ?? 0)) {
-              mostRecentFile = file;
-            }
-          }
-        }
+    final result = await Computer.shared().compute(
+      _findRecentFileAndGenerateTempMarkers,
+      param: {"files": files, "center": widget.center},
+    );
 
 
-        tempMarkers.add(
-          ImageMarker(
-            latitude: file.location!.latitude!,
-            longitude: file.location!.longitude!,
-            imageFile: file,
-          ),
-        );
-      }
-    }
+    final EnteFile? mostRecentFile = result.$1;
+    final List<ImageMarker> tempMarkers = result.$2;
 
 
-    if (hasAnyLocation) {
+    if (tempMarkers.isNotEmpty) {
       center = widget.center ??
       center = widget.center ??
           LatLng(
           LatLng(
             mostRecentFile!.location!.latitude!,
             mostRecentFile!.location!.latitude!,
@@ -173,6 +146,50 @@ class _MapScreenState extends State<MapScreen> {
     });
     });
   }
   }
 
 
+  static (EnteFile?, List<ImageMarker>) _findRecentFileAndGenerateTempMarkers(
+    Map<String, dynamic> args,
+  ) {
+    final Logger logger = Logger("_MapScreenState");
+    final files = args["files"] as List<EnteFile>;
+    final center = args["center"] as LatLng?;
+    final List<ImageMarker> tempMarkers = [];
+    EnteFile? mostRecentFile;
+
+    for (var file in files) {
+      if (file.hasLocation) {
+        if (!Location.isValidRange(
+          latitude: file.location!.latitude!,
+          longitude: file.location!.longitude!,
+        )) {
+          logger.warning(
+            'Skipping file with invalid location ${file.toString()}',
+          );
+          continue;
+        }
+
+        if (center == null) {
+          if (mostRecentFile == null) {
+            mostRecentFile = file;
+          } else {
+            if ((mostRecentFile.creationTime ?? 0) < (file.creationTime ?? 0)) {
+              mostRecentFile = file;
+            }
+          }
+        }
+
+        tempMarkers.add(
+          ImageMarker(
+            latitude: file.location!.latitude!,
+            longitude: file.location!.longitude!,
+            imageFile: file,
+          ),
+        );
+      }
+    }
+
+    return (mostRecentFile, tempMarkers);
+  }
+
   @pragma('vm:entry-point')
   @pragma('vm:entry-point')
   static void _calculateMarkersIsolate(MapIsolate message) async {
   static void _calculateMarkersIsolate(MapIsolate message) async {
     final bounds = message.bounds;
     final bounds = message.bounds;

+ 0 - 10
lib/ui/viewer/file_details/location_tags_widget.dart

@@ -14,9 +14,7 @@ import "package:photos/services/location_service.dart";
 import "package:photos/services/search_service.dart";
 import "package:photos/services/search_service.dart";
 import "package:photos/services/user_remote_flag_service.dart";
 import "package:photos/services/user_remote_flag_service.dart";
 import "package:photos/states/location_screen_state.dart";
 import "package:photos/states/location_screen_state.dart";
-import "package:photos/theme/colors.dart";
 import "package:photos/theme/ente_theme.dart";
 import "package:photos/theme/ente_theme.dart";
-import "package:photos/ui/common/loading_widget.dart";
 import "package:photos/ui/components/buttons/chip_button_widget.dart";
 import "package:photos/ui/components/buttons/chip_button_widget.dart";
 import "package:photos/ui/components/info_item_widget.dart";
 import "package:photos/ui/components/info_item_widget.dart";
 import "package:photos/ui/map/enable_map.dart";
 import "package:photos/ui/map/enable_map.dart";
@@ -255,14 +253,6 @@ class _InfoMapState extends State<InfoMap> {
                         ),
                         ),
                       ),
                       ),
                     ),
                     ),
-                    _tappedToOpenMap
-                        ? const EnteLoadingWidget(
-                            alignment: Alignment.topLeft,
-                            padding: 19,
-                            size: 11,
-                            color: strokeSolidMutedLight,
-                          )
-                        : const SizedBox.shrink(),
                   ],
                   ],
                 )
                 )
               : ValueListenableBuilder(
               : ValueListenableBuilder(

+ 60 - 0
lib/ui/viewer/gallery/gallery_app_bar_widget.dart

@@ -6,10 +6,14 @@ import "package:flutter/cupertino.dart";
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/configuration.dart';
+import "package:photos/core/constants.dart";
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/core/event_bus.dart';
+import "package:photos/core/network/network.dart";
 import "package:photos/db/files_db.dart";
 import "package:photos/db/files_db.dart";
 import 'package:photos/events/subscription_purchased_event.dart';
 import 'package:photos/events/subscription_purchased_event.dart';
+import "package:photos/gateways/cast_gw.dart";
 import "package:photos/generated/l10n.dart";
 import "package:photos/generated/l10n.dart";
+import "package:photos/l10n/l10n.dart";
 import 'package:photos/models/backup_status.dart';
 import 'package:photos/models/backup_status.dart';
 import 'package:photos/models/collection/collection.dart';
 import 'package:photos/models/collection/collection.dart';
 import 'package:photos/models/device_collection.dart';
 import 'package:photos/models/device_collection.dart';
@@ -36,6 +40,7 @@ import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/magic_util.dart';
 import 'package:photos/utils/magic_util.dart';
 import 'package:photos/utils/navigation_util.dart';
 import 'package:photos/utils/navigation_util.dart';
 import 'package:photos/utils/toast_util.dart';
 import 'package:photos/utils/toast_util.dart';
+import "package:uuid/uuid.dart";
 
 
 class GalleryAppBarWidget extends StatefulWidget {
 class GalleryAppBarWidget extends StatefulWidget {
   final GalleryType type;
   final GalleryType type;
@@ -64,6 +69,7 @@ enum AlbumPopupAction {
   ownedArchive,
   ownedArchive,
   sharedArchive,
   sharedArchive,
   ownedHide,
   ownedHide,
+  playOnTv,
   sort,
   sort,
   leave,
   leave,
   freeUpSpace,
   freeUpSpace,
@@ -472,6 +478,22 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
         ),
         ),
       );
       );
     }
     }
+    if (widget.collection != null && isInternalUser) {
+      items.add(
+        PopupMenuItem(
+          value: AlbumPopupAction.playOnTv,
+          child: Row(
+            children: [
+              const Icon(Icons.tv_outlined),
+              const Padding(
+                padding: EdgeInsets.all(8),
+              ),
+              Text(context.l10n.playOnTv),
+            ],
+          ),
+        ),
+      );
+    }
 
 
     if (galleryType.canDelete()) {
     if (galleryType.canDelete()) {
       items.add(
       items.add(
@@ -579,6 +601,8 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
               await _removeQuickLink();
               await _removeQuickLink();
             } else if (value == AlbumPopupAction.leave) {
             } else if (value == AlbumPopupAction.leave) {
               await _leaveAlbum(context);
               await _leaveAlbum(context);
+            } else if (value == AlbumPopupAction.playOnTv) {
+              await castAlbum();
             } else if (value == AlbumPopupAction.freeUpSpace) {
             } else if (value == AlbumPopupAction.freeUpSpace) {
               await _deleteBackedUpFiles(context);
               await _deleteBackedUpFiles(context);
             } else if (value == AlbumPopupAction.setCover) {
             } else if (value == AlbumPopupAction.setCover) {
@@ -797,4 +821,40 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
     );
     );
     setState(() {});
     setState(() {});
   }
   }
+
+  Future<void> castAlbum() async {
+    final gw = CastGateway(NetworkClient.instance.enteDio);
+    // stop any existing cast session
+    gw.revokeAllTokens().ignore();
+    await showTextInputDialog(
+      context,
+      title: context.l10n.playOnTv,
+      body: S.of(context).castInstruction,
+      submitButtonLabel: S.of(context).pair,
+      textInputType: TextInputType.streetAddress,
+      hintText: context.l10n.deviceCodeHint,
+      onSubmit: (String text) async {
+        try {
+          String code = text.trim();
+          final String? publicKey = await gw.getPublicKey(code);
+          if (publicKey == null) {
+            showToast(context, S.of(context).deviceNotFound);
+            return;
+          }
+          final String castToken = Uuid().v4().toString();
+          final castPayload = CollectionsService.instance
+              .getCastData(castToken, widget.collection!, publicKey);
+          await gw.publishCastPayload(
+            code,
+            castPayload,
+            widget.collection!.id,
+            castToken,
+          );
+        } catch (e, s) {
+          _logger.severe("Failed to cast album", e, s);
+          await showGenericErrorDialog(context: context, error: e);
+        }
+      },
+    );
+  }
 }
 }

+ 39 - 1
lib/utils/exif_util.dart

@@ -11,6 +11,11 @@ import 'package:photos/utils/file_util.dart';
 
 
 const kDateTimeOriginal = "EXIF DateTimeOriginal";
 const kDateTimeOriginal = "EXIF DateTimeOriginal";
 const kImageDateTime = "Image DateTime";
 const kImageDateTime = "Image DateTime";
+const kExifOffSetKeys = [
+  "EXIF OffsetTime",
+  "EXIF OffsetTimeOriginal",
+  "EXIF OffsetTimeDigitized",
+];
 const kExifDateTimePattern = "yyyy:MM:dd HH:mm:ss";
 const kExifDateTimePattern = "yyyy:MM:dd HH:mm:ss";
 const kEmptyExifDateTime = "0000:00:00 00:00:00";
 const kEmptyExifDateTime = "0000:00:00 00:00:00";
 
 
@@ -56,7 +61,14 @@ Future<DateTime?> getCreationTimeFromEXIF(
             ? exif[kImageDateTime]!.printable
             ? exif[kImageDateTime]!.printable
             : null;
             : null;
     if (exifTime != null && exifTime != kEmptyExifDateTime) {
     if (exifTime != null && exifTime != kEmptyExifDateTime) {
-      return DateFormat(kExifDateTimePattern).parse(exifTime);
+      String? exifOffsetTime;
+      for (final key in kExifOffSetKeys) {
+        if (exif.containsKey(key)) {
+          exifOffsetTime = exif[key]!.printable;
+          break;
+        }
+      }
+      return getDateTimeInDeviceTimezone(exifTime, exifOffsetTime);
     }
     }
   } catch (e) {
   } catch (e) {
     _logger.severe("failed to getCreationTimeFromEXIF", e);
     _logger.severe("failed to getCreationTimeFromEXIF", e);
@@ -64,6 +76,32 @@ Future<DateTime?> getCreationTimeFromEXIF(
   return null;
   return null;
 }
 }
 
 
+DateTime getDateTimeInDeviceTimezone(String exifTime, String? offsetString) {
+  final DateTime result = DateFormat(kExifDateTimePattern).parse(exifTime);
+  if (offsetString == null) {
+    return result;
+  }
+  try {
+    final List<String> splitHHMM = offsetString.split(":");
+    // Parse the offset from the photo's time zone
+    final int offsetHours = int.parse(splitHHMM[0]);
+    final int offsetMinutes =
+        int.parse(splitHHMM[1]) * (offsetHours.isNegative ? -1 : 1);
+    // Adjust the date for the offset to get the photo's correct UTC time
+    final photoUtcDate =
+        result.add(Duration(hours: -offsetHours, minutes: -offsetMinutes));
+    // Getting the current device's time zone offset from UTC
+    final now = DateTime.now();
+    final localOffset = now.timeZoneOffset;
+    // Adjusting the photo's UTC time to the device's local time
+    final deviceLocalTime = photoUtcDate.add(localOffset);
+    return deviceLocalTime;
+  } catch (e, s) {
+    _logger.severe("tz offset adjust failed $offsetString", e, s);
+  }
+  return result;
+}
+
 Location? locationFromExif(Map<String, IfdTag> exif) {
 Location? locationFromExif(Map<String, IfdTag> exif) {
   try {
   try {
     return gpsDataFromExif(exif).toLocationObj();
     return gpsDataFromExif(exif).toLocationObj();

+ 1 - 1
pubspec.yaml

@@ -12,7 +12,7 @@ description: ente photos application
 # Read more about iOS versioning at
 # Read more about iOS versioning at
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 
 
-version: 0.8.51+571
+version: 0.8.52+572
 
 
 environment:
 environment:
   sdk: ">=3.0.0 <4.0.0"
   sdk: ">=3.0.0 <4.0.0"