Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
35 changes: 35 additions & 0 deletions README-ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,41 @@ iCloud 文件只能在设备上的 Apple ID 正常登录时获取。
当账号要求重新输入密码验证时,未缓存在本地的 iCloud 文件将无法访问,
此时相关方法会抛出 `CloudPhotoLibraryErrorDomain` 错误。

#### 获取云标识符用于跨设备资源识别 (iOS 15+ / macOS 12+)

> [!NOTE]
> 此功能仅在 iOS 15+ 和 macOS 12+ 上可用。

当使用 iCloud 照片图库时,相同的资源可能会出现在多个设备上,
但具有不同的本地标识符。云标识符提供了一种稳定的方式来识别
共享同一 iCloud 账户的设备上的相同资源。

```dart
// 批量高效获取多个资源的云标识符
final List<AssetEntity> 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]
Expand Down
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<AssetEntity> 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]
Expand Down
4 changes: 4 additions & 0 deletions darwin/photo_manager/Sources/photo_manager/PMPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSString *> *localIdentifiers = call.arguments[@"localIdentifiers"];
NSDictionary<NSString *, NSString *> *cloudIdentifiers = [manager getCloudIdentifiersForLocalIdentifiers:localIdentifiers];
[handler reply:cloudIdentifiers];
} else {
[handler notImplemented];
}
Expand Down
3 changes: 3 additions & 0 deletions darwin/photo_manager/Sources/photo_manager/core/PMManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,7 @@ typedef void (^AssetBlockResult)(PMAssetEntity *, NSObject *);

// cancelAllRequest
- (void)cancelAllRequest;

// getCloudIdentifiers
- (NSDictionary<NSString *, NSString *> *)getCloudIdentifiersForLocalIdentifiers:(NSArray<NSString *> *)localIdentifiers;
@end
85 changes: 85 additions & 0 deletions darwin/photo_manager/Sources/photo_manager/core/PMManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -2021,4 +2021,89 @@ - (void)injectModifyToDate:(PMAssetPathEntity *)path {
}
}

#pragma mark cloud identifiers

- (NSDictionary<NSString *, NSString *> *)getCloudIdentifiersForLocalIdentifiers:(NSArray<NSString *> *)localIdentifiers {
NSMutableDictionary<NSString *, NSString *> *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
6 changes: 6 additions & 0 deletions lib/platform_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
1 change: 1 addition & 0 deletions lib/src/internal/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
23 changes: 23 additions & 0 deletions lib/src/internal/plugin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Map<String, String?>> iosGetCloudIdentifiers(
List<String> localIdentifiers,
) async {
assert(PlatformUtils.isIOS || PlatformUtils.isMacOS);
final Map result = await _channel.invokeMethod(
PMConstants.mGetCloudIdentifiers,
<String, dynamic>{
'localIdentifiers': localIdentifiers,
},
);
return result.cast<String, String?>();
}
}

mixin AndroidPlugin on BasePlugin {
Expand Down
40 changes: 40 additions & 0 deletions lib/src/managers/photo_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -298,4 +299,43 @@ class PhotoManager {

/// Cancel all loading.
static Future<void> 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<AssetEntity> 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<Map<String, String?>> getCloudIdentifiers(
List<String> localIdentifiers,
) async {
if (PlatformUtils.isIOS || PlatformUtils.isMacOS) {
return plugin.iosGetCloudIdentifiers(localIdentifiers);
}
return <String, String?>{};
}
}
34 changes: 34 additions & 0 deletions lib/src/types/entity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String?> 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<String?> get cloudIdentifier async {
if (PlatformUtils.isIOS || PlatformUtils.isMacOS) {
final Map<String, String?> result =
await plugin.iosGetCloudIdentifiers(<String>[id]);
return result[id];
}
return null;
}

AssetEntity copyWith({
String? id,
int? typeInt,
Expand Down
Loading