diff --git a/CHANGELOG.md b/CHANGELOG.md index c1a123cf..06a5fbf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,13 @@ To know more about breaking changes, see the [Migration Guide][]. ## Unreleased -*None*. +**Features** + +- Add `getCloudIdentifiers` method to `PhotoManager` for iOS 15+ and macOS 12+. + - Retrieves cloud identifiers for assets that are stable across devices with the same iCloud Photo Library. + - Returns a map of local identifiers to their corresponding cloud identifiers. + - Useful for identifying the same asset across different devices sharing an iCloud account. +- Add `cloudIdentifier` getter to `AssetEntity` for convenient access to a single asset's cloud identifier. ## 3.8.0 diff --git a/README-ZH.md b/README-ZH.md index 5b62279a..5a3c5fc4 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -431,6 +431,41 @@ iCloud 文件只能在设备上的 Apple ID 正常登录时获取。 当账号要求重新输入密码验证时,未缓存在本地的 iCloud 文件将无法访问, 此时相关方法会抛出 `CloudPhotoLibraryErrorDomain` 错误。 +#### 获取云标识符用于跨设备资源识别 (iOS 15+ / macOS 12+) + +> [!NOTE] +> 此功能仅在 iOS 15+ 和 macOS 12+ 上可用。 + +当使用 iCloud 照片图库时,相同的资源可能会出现在多个设备上, +但具有不同的本地标识符。云标识符提供了一种稳定的方式来识别 +共享同一 iCloud 账户的设备上的相同资源。 + +```dart +// 批量高效获取多个资源的云标识符 +final List assets = await path.getAssetListPaged(page: 0, size: 10); +final localIds = assets.map((e) => e.id).toList(); +final cloudIds = await PhotoManager.getCloudIdentifiers(localIds); + +for (final asset in assets) { + final cloudId = cloudIds[asset.id]; + if (cloudId != null) { + print('资源 ${asset.id} 的云标识符为: $cloudId'); + // 存储 cloudId 以便在其他设备上识别相同的资源 + } +} + +// 或者获取单个资源的云标识符 +final cloudId = await asset.cloudIdentifier; +``` + +云标识符在以下场景中很有用: +- 在多个设备之间同步用户偏好(例如,收藏、编辑) +- 跟踪用户已处理过的资源 +- 在使用相同 iCloud 照片图库的设备之间匹配资源 + +请注意,未同步到 iCloud 或未登录 iCloud 的设备上的资源 +将具有 null 值的云标识符。 + #### 取消加载 (3.8.0 新增) > [!NOTE] diff --git a/README.md b/README.md index c8e5b4f1..e317dd82 100644 --- a/README.md +++ b/README.md @@ -467,6 +467,41 @@ When the account requires to re-enter the password to verify, iCloud files that locally available are not allowed to be fetched. The photo library will throws `CloudPhotoLibraryErrorDomain` in this circumstance. +#### Get cloud identifiers for cross-device asset identification (iOS 15+ / macOS 12+) + +> [!NOTE] +> This feature is only available on iOS 15+ and macOS 12+. + +When using iCloud Photo Library, the same asset can appear on multiple devices +but with different local identifiers. Cloud identifiers provide a stable way +to identify the same asset across devices that share the same iCloud account. + +```dart +// Get cloud identifiers for multiple assets efficiently +final List assets = await path.getAssetListPaged(page: 0, size: 10); +final localIds = assets.map((e) => e.id).toList(); +final cloudIds = await PhotoManager.getCloudIdentifiers(localIds); + +for (final asset in assets) { + final cloudId = cloudIds[asset.id]; + if (cloudId != null) { + print('Asset ${asset.id} has cloud ID: $cloudId'); + // Store cloudId to identify the same asset on other devices + } +} + +// Or get cloud identifier for a single asset +final cloudId = await asset.cloudIdentifier; +``` + +Cloud identifiers are useful when you need to: +- Sync user preferences (e.g., favorites, edits) across devices +- Track which assets a user has already processed +- Match assets between devices with the same iCloud Photo Library + +Note that assets not synced to iCloud or on devices not signed into iCloud +will have null cloud identifiers. + #### Cancel loading (Since 3.8.0) > [!NOTE] diff --git a/darwin/photo_manager/Sources/photo_manager/PMPlugin.m b/darwin/photo_manager/Sources/photo_manager/PMPlugin.m index 1993bd2a..9f587a41 100644 --- a/darwin/photo_manager/Sources/photo_manager/PMPlugin.m +++ b/darwin/photo_manager/Sources/photo_manager/PMPlugin.m @@ -708,6 +708,10 @@ - (void)handleMethodResultHandler:(PMResultHandler *)handler manager:(PMManager } else if ([@"cancelAllRequest" isEqualToString:call.method]) { [manager cancelAllRequest]; [handler reply:@YES]; + } else if ([@"getCloudIdentifiers" isEqualToString:call.method]) { + NSArray *localIdentifiers = call.arguments[@"localIdentifiers"]; + NSDictionary *cloudIdentifiers = [manager getCloudIdentifiersForLocalIdentifiers:localIdentifiers]; + [handler reply:cloudIdentifiers]; } else { [handler notImplemented]; } diff --git a/darwin/photo_manager/Sources/photo_manager/core/PMManager.h b/darwin/photo_manager/Sources/photo_manager/core/PMManager.h index 85ece3d8..d7056c66 100644 --- a/darwin/photo_manager/Sources/photo_manager/core/PMManager.h +++ b/darwin/photo_manager/Sources/photo_manager/core/PMManager.h @@ -143,4 +143,7 @@ typedef void (^AssetBlockResult)(PMAssetEntity *, NSObject *); // cancelAllRequest - (void)cancelAllRequest; + +// getCloudIdentifiers +- (NSDictionary *)getCloudIdentifiersForLocalIdentifiers:(NSArray *)localIdentifiers; @end diff --git a/darwin/photo_manager/Sources/photo_manager/core/PMManager.m b/darwin/photo_manager/Sources/photo_manager/core/PMManager.m index c8a4e0d4..7130e9b6 100644 --- a/darwin/photo_manager/Sources/photo_manager/core/PMManager.m +++ b/darwin/photo_manager/Sources/photo_manager/core/PMManager.m @@ -2021,4 +2021,89 @@ - (void)injectModifyToDate:(PMAssetPathEntity *)path { } } +#pragma mark cloud identifiers + +- (NSDictionary *)getCloudIdentifiersForLocalIdentifiers:(NSArray *)localIdentifiers { + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + + // Check if the API is available (iOS 15+, macOS 12+) +#if TARGET_OS_IOS + if (@available(iOS 15.0, *)) { + // API is available, proceed + } else { + // Return empty dictionary for older versions + return result; + } +#elif TARGET_OS_OSX + if (@available(macOS 12.0, *)) { + // API is available, proceed + } else { + // Return empty dictionary for older versions + return result; + } +#else + // Unsupported platform + return result; +#endif + + // Use dispatch_semaphore to wait for the async call + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + __block NSDictionary *mappings = nil; + + // Must call on main thread + dispatch_async(dispatch_get_main_queue(), ^{ +#if TARGET_OS_IOS + if (@available(iOS 15.0, *)) { + [[PHPhotoLibrary sharedPhotoLibrary] cloudIdentifierMappingsForLocalIdentifiers:localIdentifiers + completionHandler:^(NSDictionary * _Nonnull cloudIdentifierMappings) { + mappings = cloudIdentifierMappings; + dispatch_semaphore_signal(semaphore); + }]; + } else { + dispatch_semaphore_signal(semaphore); + } +#elif TARGET_OS_OSX + if (@available(macOS 12.0, *)) { + [[PHPhotoLibrary sharedPhotoLibrary] cloudIdentifierMappingsForLocalIdentifiers:localIdentifiers + completionHandler:^(NSDictionary * _Nonnull cloudIdentifierMappings) { + mappings = cloudIdentifierMappings; + dispatch_semaphore_signal(semaphore); + }]; + } else { + dispatch_semaphore_signal(semaphore); + } +#else + dispatch_semaphore_signal(semaphore); +#endif + }); + + // Wait for the async call to complete + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + + // Convert the mappings to a dictionary + if (mappings) { + for (NSString *localId in mappings) { + id mapping = mappings[localId]; + // Use runtime method lookup to get cloudIdentifier + if ([mapping respondsToSelector:@selector(cloudIdentifier)]) { + id cloudIdentifier = [mapping valueForKey:@"cloudIdentifier"]; + if (cloudIdentifier && [cloudIdentifier respondsToSelector:@selector(stringValue)]) { + NSString *stringValue = [cloudIdentifier valueForKey:@"stringValue"]; + if (stringValue) { + result[localId] = stringValue; + } else { + result[localId] = (NSString *)[NSNull null]; + } + } else { + result[localId] = (NSString *)[NSNull null]; + } + } else { + result[localId] = (NSString *)[NSNull null]; + } + } + } + + return result; +} + @end diff --git a/lib/platform_utils.dart b/lib/platform_utils.dart index 0af5bf1d..7280a1cc 100644 --- a/lib/platform_utils.dart +++ b/lib/platform_utils.dart @@ -13,4 +13,10 @@ class PlatformUtils { /// Whether the operating system is a version of /// [ohos](https://en.wikipedia.org/wiki/OpenHarmony). static final isOhos = Platform.operatingSystem == 'ohos'; + + /// Whether the operating system is iOS. + static final isIOS = Platform.isIOS; + + /// Whether the operating system is macOS. + static final isMacOS = Platform.isMacOS; } diff --git a/lib/src/internal/constants.dart b/lib/src/internal/constants.dart index 0d1891b2..eea437da 100644 --- a/lib/src/internal/constants.dart +++ b/lib/src/internal/constants.dart @@ -69,6 +69,7 @@ class PMConstants { static const String mCancelRequestWithCancelToken = 'cancelRequestWithCancelToken'; static const String mCancelAllRequest = 'cancelAllRequest'; + static const String mGetCloudIdentifiers = 'getCloudIdentifiers'; /// Constant value. static const int vDefaultThumbnailSize = 150; diff --git a/lib/src/internal/plugin.dart b/lib/src/internal/plugin.dart index 6e009fac..3da93134 100644 --- a/lib/src/internal/plugin.dart +++ b/lib/src/internal/plugin.dart @@ -911,6 +911,29 @@ mixin IosPlugin on BasePlugin { ); return result['errorMsg'] == null; } + + /// Get cloud identifiers for the given local identifiers. + /// + /// This method retrieves cloud identifier mappings for assets with the same + /// iCloud account across different devices. Only available on iOS 15+ and macOS 12+. + /// + /// Returns a map of local identifiers to their corresponding cloud identifiers. + /// If an asset doesn't have a cloud identifier (e.g., not in iCloud), the value will be null. + /// + /// See also: + /// * https://developer.apple.com/documentation/photokit/phphotolibrary/3750728-cloudidentifiermappingsforlocali + Future> iosGetCloudIdentifiers( + List localIdentifiers, + ) async { + assert(PlatformUtils.isIOS || PlatformUtils.isMacOS); + final Map result = await _channel.invokeMethod( + PMConstants.mGetCloudIdentifiers, + { + 'localIdentifiers': localIdentifiers, + }, + ); + return result.cast(); + } } mixin AndroidPlugin on BasePlugin { diff --git a/lib/src/managers/photo_manager.dart b/lib/src/managers/photo_manager.dart index 423102bf..c04fab9a 100644 --- a/lib/src/managers/photo_manager.dart +++ b/lib/src/managers/photo_manager.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import '../../platform_utils.dart'; import '../filter/base_filter.dart'; import '../filter/path_filter.dart'; import '../internal/editor.dart'; @@ -298,4 +299,43 @@ class PhotoManager { /// Cancel all loading. static Future cancelAllRequest() => plugin.cancelAllRequest(); + + /// Get cloud identifiers for the given local identifiers (iOS 15+ and macOS 12+ only). + /// + /// This method retrieves cloud identifier mappings for assets with the same + /// iCloud account across different devices. Cloud identifiers are stable + /// across devices that share the same iCloud Photo Library. + /// + /// **Important:** This method is only available on iOS 15+ and macOS 12+. + /// On other platforms or older versions, an empty map will be returned. + /// + /// The [localIdentifiers] parameter should contain the [AssetEntity.id] values + /// of the assets you want to get cloud identifiers for. + /// + /// Returns a map where keys are local identifiers and values are cloud identifiers. + /// If an asset doesn't have a cloud identifier (e.g., not synced to iCloud), + /// the value will be null. + /// + /// Example: + /// ```dart + /// final List assets = await path.getAssetListPaged(page: 0, size: 10); + /// final localIds = assets.map((e) => e.id).toList(); + /// final cloudIds = await PhotoManager.getCloudIdentifiers(localIds); + /// + /// for (final asset in assets) { + /// final cloudId = cloudIds[asset.id]; + /// print('Local: ${asset.id}, Cloud: $cloudId'); + /// } + /// ``` + /// + /// See also: + /// * https://developer.apple.com/documentation/photokit/phphotolibrary/3750728-cloudidentifiermappingsforlocali + static Future> getCloudIdentifiers( + List localIdentifiers, + ) async { + if (PlatformUtils.isIOS || PlatformUtils.isMacOS) { + return plugin.iosGetCloudIdentifiers(localIdentifiers); + } + return {}; + } } diff --git a/lib/src/types/entity.dart b/lib/src/types/entity.dart index 53ffcbcc..11fab798 100644 --- a/lib/src/types/entity.dart +++ b/lib/src/types/entity.dart @@ -906,6 +906,40 @@ class AssetEntity { /// * https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/understanding_utis/understand_utis_conc/understand_utis_conc.html#//apple_ref/doc/uid/TP40001319-CH202-SW1 Future get mimeTypeAsync => plugin.getMimeTypeAsync(this); + /// Get the cloud identifier for this asset (iOS 15+ and macOS 12+ only). + /// + /// This method retrieves the cloud identifier for this asset which is stable + /// across devices that share the same iCloud Photo Library. This is useful + /// for identifying the same asset on different devices. + /// + /// Returns the cloud identifier string, or null if the asset doesn't have + /// a cloud identifier (e.g., not synced to iCloud) or if the platform + /// doesn't support this feature. + /// + /// **Important:** This method is only available on iOS 15+ and macOS 12+. + /// On other platforms or older versions, it will return null. + /// + /// Example: + /// ```dart + /// final cloudId = await asset.cloudIdentifier; + /// if (cloudId != null) { + /// print('Cloud ID: $cloudId'); + /// // Save this ID to sync with other devices + /// } + /// ``` + /// + /// See also: + /// * [PhotoManager.getCloudIdentifiers] to get cloud identifiers for multiple assets efficiently. + /// * https://developer.apple.com/documentation/photokit/phphotolibrary/3750728-cloudidentifiermappingsforlocali + Future get cloudIdentifier async { + if (PlatformUtils.isIOS || PlatformUtils.isMacOS) { + final Map result = + await plugin.iosGetCloudIdentifiers([id]); + return result[id]; + } + return null; + } + AssetEntity copyWith({ String? id, int? typeInt,