import Flutter import StoreKit import Photos import UIKit public class ClassifyPhotoPlugin: NSObject, FlutterPlugin { var photoClassifier = ClassifyPhoto() public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "classify_photo", binaryMessenger: registrar.messenger()) let instance = ClassifyPhotoPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { print("iOS: Received method call: \(call.method)") switch call.method { case "getPhoto": self.getPhoto(flutterResult: result) case "getStorageInfo": getStorageInfo(result: result) case "getExifInfo": guard let args = call.arguments as? [String: Any], let filePath = args["filePath"] as? String else { result(FlutterError( code: "INVALID_ARGUMENTS", message: "Missing filePath parameter", details: nil )) return } getExifInfo(filePath: filePath, completion: result) case "getPhotosSize": guard let args = call.arguments as? [String: Any], let assetIds = args["assetIds"] as? [String] else { result(FlutterError( code: "INVALID_ARGUMENTS", message: "Missing filePath parameter", details: nil )) return } calculatePhotosSize(assetIds: assetIds, completion: result) case "getPlatformVersion": result("iOS " + UIDevice.current.systemVersion) default: result(FlutterMethodNotImplemented) } } private class func blankof(type:T.Type) -> T { let ptr = UnsafeMutablePointer.allocate(capacity: MemoryLayout.size) let val = ptr.pointee return val } /// 磁盘总大小 private class func getTotalDiskSize() -> Int64 { var fs = blankof(type: statfs.self) if statfs("/var",&fs) >= 0{ return Int64(UInt64(fs.f_bsize) * fs.f_blocks) } return -1 } private func getStorageInfo(result: @escaping FlutterResult) { DispatchQueue.global(qos: .userInitiated).async { var storageInfo: [String: Int64] = [:] // 获取总容量和可用容量 if let space = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory()) { let totalSpace = ClassifyPhotoPlugin.getTotalDiskSize() let freeSpace = space[.systemFreeSize] as? Int64 ?? 0 storageInfo["totalSpace"] = totalSpace storageInfo["freeSpace"] = freeSpace storageInfo["usedSpace"] = totalSpace - freeSpace } // 获取照片占用的空间 let options = PHFetchOptions() let allPhotos = PHAsset.fetchAssets(with: .image, options: options) var photoSize: Int64 = 0 let group = DispatchGroup() let queue = DispatchQueue(label: "com.app.photosize", attributes: .concurrent) let semaphore = DispatchSemaphore(value: 10) // 限制并发 allPhotos.enumerateObjects { (asset, index, stop) in group.enter() semaphore.wait() let resources = PHAssetResource.assetResources(for: asset) if let resource = resources.first { queue.async { let options = PHAssetResourceRequestOptions() options.isNetworkAccessAllowed = true PHAssetResourceManager.default().requestData( for: resource, options: options, dataReceivedHandler: { data in photoSize += Int64(data.count) }, completionHandler: { error in if let error = error { print("Error getting photo size: \(error)") } semaphore.signal() group.leave() } ) } } else { semaphore.signal() group.leave() } } group.notify(queue: .main) { storageInfo["photoSpace"] = photoSize result(storageInfo) } } } private func getPhoto(flutterResult: @escaping FlutterResult) { DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let self = self else { return } let fetchOptions = PHFetchOptions() let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions) photoClassifier.classifyPhotos( assets: allPhotos, progressHandler: { (stage, progress) in print("Progress: \(stage) - \(progress)") }, completion: { result in var resultData: [[String: Any]] = [] let mainGroup = DispatchGroup() // 处理截图 mainGroup.enter() self.processPhotoGroup(assets: result.screenshots, groupName: "screenshots", sizeInfo: result.screenshotsSize) { groupData in if !groupData.isEmpty { resultData.append(["group": groupData, "type": "screenshots"]) } mainGroup.leave() } // 处理相似照片组 for photoGroup in result.similarPhotos { mainGroup.enter() self.processPhotoGroup(assets: photoGroup, groupName: "similar", sizeInfo: result.similarPhotosSize) { groupData in if !groupData.isEmpty { resultData.append(["group": groupData, "type": "similar"]) } mainGroup.leave() } } // 处理地点分组 for (location, assets) in result.locations { mainGroup.enter() self.processPhotoGroup(assets: assets, groupName: location, sizeInfo: result.locationsSize) { groupData in if !groupData.isEmpty { resultData.append(["group": groupData, "type": "location", "name": location]) } mainGroup.leave() } } // 处理人物分组 for (person, assets) in result.people { mainGroup.enter() self.processPhotoGroup(assets: assets, groupName: person, sizeInfo: result.peopleSize) { groupData in if !groupData.isEmpty { resultData.append(["group": groupData, "type": "people"]) } mainGroup.leave() } } // 处理模糊照片 mainGroup.enter() self.processPhotoGroup(assets: result.blurryPhotos, groupName: "blurry", sizeInfo: result.blurryPhotosSize) { groupData in if !groupData.isEmpty { resultData.append(["group": groupData, "type": "blurry"]) } mainGroup.leave() } mainGroup.notify(queue: .main) { print("Final result count: \(resultData.count)") flutterResult(resultData) } } ) } } // 处理照片组的辅助方法 private func processPhotoGroup( assets: [PHAsset], groupName: String, sizeInfo: ClassifyPhoto.PhotoSizeInfo, completion: @escaping ([String: Any]) -> Void ) { let photoProcessGroup = DispatchGroup() var photosData: [[String: Any]] = [] for asset in assets { photoProcessGroup.enter() let options = PHContentEditingInputRequestOptions() options.isNetworkAccessAllowed = true asset.requestContentEditingInput(with: options) { (input, info) in defer { photoProcessGroup.leave() } if let input = input, let url = input.fullSizeImageURL { let photoInfo: [String: Any] = [ "path": url.path, "id": asset.localIdentifier, "width": asset.pixelWidth, "height": asset.pixelHeight, "creationDate": asset.creationDate?.timeIntervalSince1970 ?? 0 ] photosData.append(photoInfo) } } } photoProcessGroup.notify(queue: .main) { completion([ "photos": photosData, "totalSize": sizeInfo.totalSize, "count": sizeInfo.count ]) } } private func getExifInfo(filePath: String, completion: @escaping FlutterResult) { // 创建文件 URL let fileURL: URL if filePath.starts(with: "file://") { guard let url = URL(string: filePath) else { print("Invalid URL string: \(filePath)") completion([:]) return } fileURL = url } else { fileURL = URL(fileURLWithPath: filePath) } // 检查文件是否存在 guard FileManager.default.fileExists(atPath: fileURL.path) else { print("File does not exist at path: \(fileURL.path)") completion([:]) return } // 创建图片源 guard let imageSource = CGImageSourceCreateWithURL(fileURL as CFURL, nil) else { print("Failed to create image source for path: \(fileURL.path)") completion([:]) return } var exifInfo: [String: Any] = [:] // 获取所有元数据 if let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any] { // EXIF 数据 if let exif = imageProperties[kCGImagePropertyExifDictionary as String] as? [String: Any] { exifInfo["aperture"] = exif[kCGImagePropertyExifFNumber as String] exifInfo["exposureTime"] = exif[kCGImagePropertyExifExposureTime as String] exifInfo["iso"] = exif[kCGImagePropertyExifISOSpeedRatings as String] exifInfo["focalLength"] = exif[kCGImagePropertyExifFocalLength as String] exifInfo["dateTimeOriginal"] = exif[kCGImagePropertyExifDateTimeOriginal as String] } // TIFF 数据 if let tiff = imageProperties[kCGImagePropertyTIFFDictionary as String] as? [String: Any] { exifInfo["make"] = tiff[kCGImagePropertyTIFFMake as String] exifInfo["model"] = tiff[kCGImagePropertyTIFFModel as String] } // GPS 数据 if let gps = imageProperties[kCGImagePropertyGPSDictionary as String] as? [String: Any] { exifInfo["latitude"] = gps[kCGImagePropertyGPSLatitude as String] exifInfo["longitude"] = gps[kCGImagePropertyGPSLongitude as String] exifInfo["altitude"] = gps[kCGImagePropertyGPSAltitude as String] } // 图片基本信息 exifInfo["pixelWidth"] = imageProperties[kCGImagePropertyPixelWidth as String] exifInfo["pixelHeight"] = imageProperties[kCGImagePropertyPixelHeight as String] exifInfo["dpi"] = imageProperties[kCGImagePropertyDPIHeight as String] exifInfo["colorModel"] = imageProperties[kCGImagePropertyColorModel as String] exifInfo["profileName"] = imageProperties[kCGImagePropertyProfileName as String] } completion(exifInfo) } } // 计算选择的图片大小 extension ClassifyPhotoPlugin { private func calculatePhotosSize(assetIds: [String], completion: @escaping FlutterResult) { let assets = PHAsset.fetchAssets(withLocalIdentifiers: assetIds, options: nil) var phAssets: [PHAsset] = [] var sizes: Int64 = 0 // 用于存储文件大小 assets.enumerateObjects { (phAsset, _, _) in phAssets.append(phAsset) // 获取 PHAsset 的资源 let resources = PHAssetResource.assetResources(for: phAsset) if let resource = resources.first { // 获取文件大小 if let unsignedInt64 = resource.value(forKey: "fileSize") as? CLong { sizes += Int64(unsignedInt64) } } } // 返回文件大小到 Flutter completion(sizes) } } class SubscriptionHandler: NSObject { @available(iOS 15.0.0, *) func checkTrialEligibility() async -> Bool { do { // 获取产品信息 let productIds = ["clean.vip.1week"] let products = try await Product.products(for: Set(productIds)) // 检查第一个产品的试用资格 if let product = products.first { return await product.subscription?.isEligibleForIntroOffer ?? false } return false } catch { print("Error checking trial eligibility: \(error)") return false } } }