| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357 |
- 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<T>(type:T.Type) -> T {
- let ptr = UnsafeMutablePointer<T>.allocate(capacity: MemoryLayout<T>.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
- }
- }
- }
|