Преглед на файлове

fix: 获取照片方式更改

Destiny преди 9 месеца
родител
ревизия
4d87d738e3

+ 129 - 2
lib/module/home/home_controller.dart

@@ -66,7 +66,16 @@ class HomeController extends BaseController {
   Rx<AssetEntity?> blurryPhoto = Rx<AssetEntity?>(null);
 
   // 是否扫描完成
-  RxBool isScanned = false.obs;
+  RxBool isSimilarScanned = false.obs;
+
+  // 是否扫描完成
+  RxBool isPeopleScanned = false.obs;
+
+  // 是否扫描完成
+  RxBool isScreenShotScanned = false.obs;
+
+  // 是否扫描完成
+  RxBool isBlurryScanned = false.obs;
 
   // 存储是否扫描完成
   RxBool isStorageScanned = false.obs;
@@ -130,6 +139,24 @@ class HomeController extends BaseController {
       Get.toNamed(RoutePath.discount);
     }
     setFirstIntoApp(false);
+
+    if (Platform.isAndroid) {
+      loadPhotosFromDirectory();
+    }
+
+    if (await Permission.photos.request().isGranted) {
+      PhotoManager.clearFileCache();
+      getStorageInfo();
+
+      // handlePhotos();
+      await handleScreenPhotos();
+      await handleBlurryPhotos();
+      await handlePeoplePhotos();
+      await handleSimilarPhotos();
+    } else {
+
+      ToastUtil.show("Please enable the album permission");
+    }
   }
 
   @override
@@ -233,6 +260,106 @@ class HomeController extends BaseController {
     }
   }
 
+  Future<void> handleScreenPhotos() async {
+
+    final photoClassify = ClassifyPhoto();
+    try {
+      print('开始获取截图照片');
+      final photos = await photoClassify.getScreenshots();
+      print('获取截图照片完成: ${photos?.length ?? 0} 组照片');
+      isScreenShotScanned.value = true;
+      if (photos != null) {
+        await ImagePickerUtil.updatePhotos(photos);
+        if (ImagePickerUtil.screenshotPhotos.isNotEmpty) {
+          var asset = ImagePickerUtil.screenshotPhotos.first;
+          screenshotPhoto.value = asset;
+        }
+      }
+    } catch (e, stackTrace) {
+      print('获取照片失败: $e');
+      print('Stack trace: $stackTrace');
+    }
+  }
+
+  Future<void> handleBlurryPhotos() async {
+
+    final photoClassify = ClassifyPhoto();
+    try {
+      print('开始获取模糊照片');
+      final photos = await photoClassify.getBlurryPhotos();
+      print('获取模糊照片完成: ${photos?.length ?? 0} 组照片');
+      isBlurryScanned.value = true;
+      if (photos != null) {
+        await ImagePickerUtil.updatePhotos(photos);
+        if (ImagePickerUtil.blurryPhotos.isNotEmpty) {
+          var asset = ImagePickerUtil.blurryPhotos.first;
+          blurryPhoto.value = asset;
+        }
+      }
+    } catch (e, stackTrace) {
+      print('获取照片失败: $e');
+      print('Stack trace: $stackTrace');
+    }
+  }
+
+  Future<void> handlePeoplePhotos() async {
+
+    final photoClassify = ClassifyPhoto();
+    try {
+      print('开始获取人物照片');
+      final photos = await photoClassify.getPeoplePhotos();
+      print('获取人物照片完成: ${photos?.length ?? 0} 组照片');
+      isPeopleScanned.value = true;
+      if (photos != null) {
+        await ImagePickerUtil.updatePhotos(photos);
+
+        // 处理人物照片
+        peoplePhotos.clear();
+        if (ImagePickerUtil.peoplePhotos.isNotEmpty) {
+          for (var personPhotos in ImagePickerUtil.peoplePhotos) {
+            peoplePhotos.add(personPhotos);
+            if (peoplePhotos.length == 2) {
+              break;
+            }
+          }
+        }
+      }
+    } catch (e, stackTrace) {
+      print('获取照片失败: $e');
+      print('Stack trace: $stackTrace');
+    }
+  }
+
+  Future<void> handleSimilarPhotos() async {
+
+    final photoClassify = ClassifyPhoto();
+    try {
+      print('开始获取相似照片');
+      final photos = await photoClassify.getSimilarPhotos();
+      print('获取相似照片完成: ${photos?.length ?? 0} 组照片');
+      isSimilarScanned.value = true;
+      if (photos != null) {
+        await ImagePickerUtil.updatePhotos(photos);
+
+        similarPhotos.clear();
+        if (ImagePickerUtil.similarPhotos.isNotEmpty) {
+          for (var group in ImagePickerUtil.similarPhotos) {
+            for (var asset in group) {
+              similarPhotos.add(asset);
+              if (similarPhotos.length == 4) {
+                break;
+              }
+            }
+          }
+        }
+
+      }
+    } catch (e, stackTrace) {
+      print('获取照片失败: $e');
+      print('Stack trace: $stackTrace');
+    }
+  }
+
   Future<void> handlePhotos() async {
     final photoClassify = ClassifyPhoto();
     try {
@@ -241,7 +368,7 @@ class HomeController extends BaseController {
       print('获取照片完成: ${photos?.length ?? 0} 组照片');
 
       // 已完成扫描
-      isScanned.value = true;
+      // isScanned.value = true;
 
       if (photos != null) {
         await ImagePickerUtil.updatePhotos(photos);

+ 60 - 57
lib/module/home/home_view.dart

@@ -38,7 +38,7 @@ class HomePage extends BaseView<HomeController> {
               similarCard(),
               quickPhotoCard(),
               peopleCard(),
-              locationsCard(),
+              // locationsCard(),
               screenshotsAndBlurryCard(),
               SizedBox(height: 40.h),
             ],
@@ -376,52 +376,62 @@ class HomePage extends BaseView<HomeController> {
               // SizedBox(height: 19.h),
               Spacer(),
               Obx(() {
-                return Row(
-                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                  children: List.generate(4, (index) {
-                    var image = Assets.images.iconHomeNoPhoto.image(
-                      width: 70.w * 0.45,
-                      height: 70.w * 0.45,
-                    );
-                    if (controller.similarPhotos.length > index) {
-                      image = AssetEntityImage(
-                          width: 70.w,
-                          height: 70.w,
-                          controller.similarPhotos[index],
-                          isOriginal: false,
-                          thumbnailSize: const ThumbnailSize.square(300),
-                          fit: BoxFit.cover,
-                          errorBuilder: (context, error, stackTrace) {
+                return CleanUpButton(
+                  label:
+                  !controller.isSimilarScanned.value ? 'Scanning...' : 'Clean up',
+                  size: ImagePickerUtil.formatFileSize(
+                      ImagePickerUtil.similarPhotosSize.value),
+                  onTap: () {
+                    controller.similarCleanClick();
+                  },
+                );
+              }),
+            ],
+          ),
+          // SizedBox(height: 19.h),
+          Spacer(),
+          Obx(() {
+            return Row(
+              mainAxisAlignment: MainAxisAlignment.spaceBetween,
+              children: List.generate(4, (index) {
+                var image = Assets.images.iconHomeNoPhoto.image(
+                  width: 70.w * 0.45,
+                  height: 70.w * 0.45,
+                );
+                if (controller.similarPhotos.length > index) {
+                  image = AssetEntityImage(
+                      width: 70.w,
+                      height: 70.w,
+                      controller.similarPhotos[index],
+                      isOriginal: false,
+                      thumbnailSize: const ThumbnailSize.square(300),
+                      fit: BoxFit.cover,
+                      errorBuilder: (context, error, stackTrace) {
                         return Assets.images.iconHomeNoPhoto.image(
                           width: 70.w * 0.45,
                           height: 70.w * 0.45,
                         );
                       });
-                    }
-                    return controller.similarPhotos.isNotEmpty
-                        ? image
-                        : ImageContainer(
-                            size: 70.w,
-                            image: Opacity(
-                              opacity: 0.22,
-                              child: Lottie.asset(Assets.anim.animNoPhoto,
-                                  repeat: true, width: 100.w, height: 100.w),
-                            ),
-                            // AssetEntityImage(
-                            //         width: 70.w,
-                            //         height: 70.w,
-                            //         controller.similarPhotos[index],
-                            //         isOriginal: false,
-                            //         thumbnailSize: const ThumbnailSize.square(300),
-                            //         fit: BoxFit.cover,
-                            //         errorBuilder: (context, error, stackTrace) {
-                            //           return Assets.images.iconHomeNoPhoto.image(
-                            //             width: 70.w * 0.45,
-                            //             height: 70.w * 0.45,
-                            //           );
-                            //         },
-                          );
-                  }),
+                }
+                return ImageContainer(
+                  size: 70.w,
+                  image: controller.similarPhotos.length > index ? image : Opacity(
+                    opacity: 0.22,
+                    child: const CircularProgressIndicator(color: Colors.white38,)
+                  ),
+                  // AssetEntityImage(
+                  //         width: 70.w,
+                  //         height: 70.w,
+                  //         controller.similarPhotos[index],
+                  //         isOriginal: false,
+                  //         thumbnailSize: const ThumbnailSize.square(300),
+                  //         fit: BoxFit.cover,
+                  //         errorBuilder: (context, error, stackTrace) {
+                  //           return Assets.images.iconHomeNoPhoto.image(
+                  //             width: 70.w * 0.45,
+                  //             height: 70.w * 0.45,
+                  //           );
+                  //         },
                 );
               }),
               Spacer(),
@@ -503,15 +513,10 @@ class HomePage extends BaseView<HomeController> {
                           });
                         }
                         return ImageContainer(
-                          image: controller.peoplePhotos.isNotEmpty
-                              ? image
-                              : Opacity(
-                                  opacity: 0.22,
-                                  child: Lottie.asset(Assets.anim.animNoPhoto,
-                                      repeat: true,
-                                      width: 140.w,
-                                      height: 140.w),
-                                ),
+                          image: controller.peoplePhotos.length > index ? image : Opacity(
+                            opacity: 0.22,
+                            child: const CircularProgressIndicator(color: Colors.white38,),
+                          ),
                           size: 146.w,
                           // Image.file(
                           //   width: 146.w,
@@ -532,7 +537,7 @@ class HomePage extends BaseView<HomeController> {
                 right: 20.w,
                 child: Obx(() {
                   return CleanUpButton(
-                    label: !controller.isScanned.value
+                    label: !controller.isPeopleScanned.value
                         ? 'Scanning...'
                         : 'Clean up',
                     size: ImagePickerUtil.formatFileSize(
@@ -628,9 +633,7 @@ class HomePage extends BaseView<HomeController> {
                 right: 8.w,
                 child: Obx(() {
                   return CleanUpButton(
-                    label: !controller.isScanned.value
-                        ? 'Scanning...'
-                        : 'Clean up',
+                    label: 'Clean up',
                     size: ImagePickerUtil.formatFileSize(
                         ImagePickerUtil.locationsSize.value),
                     onTap: () {
@@ -654,7 +657,7 @@ class HomePage extends BaseView<HomeController> {
           children: [
             _buildCard(
               'Screenshots',
-              !controller.isScanned.value ? 'Scanning...' : 'Clean up',
+              !controller.isScreenShotScanned.value ? 'Scanning...' : 'Clean up',
               ImagePickerUtil.formatFileSize(
                   ImagePickerUtil.screenshotsSize.value),
               controller.screenshotPhoto.value == null
@@ -684,7 +687,7 @@ class HomePage extends BaseView<HomeController> {
             ),
             _buildCard(
                 'Blurry',
-                !controller.isScanned.value ? 'Scanning...' : 'Clean up',
+                !controller.isBlurryScanned.value ? 'Scanning...' : 'Clean up',
                 ImagePickerUtil.formatFileSize(
                     ImagePickerUtil.blurrySize.value),
                 controller.blurryPhoto.value == null

Файловите разлики са ограничени, защото са твърде много
+ 777 - 300
plugins/classify_photo/ios/Classes/ClassifyPhoto.swift


+ 623 - 121
plugins/classify_photo/ios/Classes/ClassifyPhotoPlugin.swift

@@ -16,6 +16,14 @@ public class ClassifyPhotoPlugin: NSObject, FlutterPlugin {
   public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
     print("iOS: Received method call: \(call.method)")
     switch call.method {
+    case "getScreenshots":
+        self.getScreenshots(flutterResult: result)
+    case "getBlurryPhotos":
+        self.getBlurryPhotos(flutterResult: result)
+    case "getPeoplePhotos":
+        self.getPeoplePhotos(flutterResult: result)
+    case "getSimilarPhotos":
+        self.getSimilarPhotos(flutterResult: result)
     case "getPhoto":
         self.getPhoto(flutterResult: result)
     case "getStorageInfo":
@@ -84,34 +92,19 @@ public class ClassifyPhotoPlugin: NSObject, FlutterPlugin {
 
             let group = DispatchGroup()
             let queue = DispatchQueue(label: "com.app.photosize", attributes: .concurrent)
-            let semaphore = DispatchSemaphore(value: 10) // 限制并发
+            let semaphore = DispatchSemaphore(value: 5) // 限制并发
 
             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()
-                            }
-                        )
+                queue.async {
+                    let resources = PHAssetResource.assetResources(for: asset)
+                    if let resource = resources.first {
+                        if let unsignedInt64 = resource.value(forKey: "fileSize") as? CLong {
+                            photoSize += Int64(unsignedInt64)
+                        }
                     }
-                } else {
                     semaphore.signal()
                     group.leave()
                 }
@@ -124,83 +117,526 @@ public class ClassifyPhotoPlugin: NSObject, FlutterPlugin {
         }
     }
 
-  private func getPhoto(flutterResult: @escaping FlutterResult) {
+    // 获取截图
+    private func getScreenshots(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)
+            
+            // 只处理截图
+            self.photoClassifier.fetchScreenshots(from: allPhotos) { screenshots in
+                // 清理内存
+                self.cleanupMemory()
+                
+                self.photoClassifier.calculateAssetsSize(screenshots) { sizeInfo in
+                    self.processPhotoGroup(
+                        assets: screenshots,
+                        groupName: "screenshots",
+                        sizeInfo: sizeInfo
+                    ) { groupData in
+                        // 再次清理内存
+                        self.cleanupMemory()
+                        
+                        DispatchQueue.main.async {
+                            if !groupData.isEmpty {
+                                flutterResult([["group": groupData, "type": "screenshots"]])
+                            } else {
+                                flutterResult([])
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
 
+    // 获取模糊照片
+    private func getBlurryPhotos(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)
+            
+            // 只处理模糊照片
+            self.photoClassifier.detectBlurryPhotos(from: allPhotos) { blurryPhotos in
+                // 清理内存
+                self.cleanupMemory()
+                
+                self.photoClassifier.calculateAssetsSize(blurryPhotos) { sizeInfo in
+                    self.processPhotoGroup(
+                        assets: blurryPhotos,
+                        groupName: "blurry",
+                        sizeInfo: sizeInfo
+                    ) { groupData in
+                        // 再次清理内存
+                        self.cleanupMemory()
+                        
+                        DispatchQueue.main.async {
+                            if !groupData.isEmpty {
+                                flutterResult([["group": groupData, "type": "blurry"]])
+                            } else {
+                                flutterResult([])
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    
+    private func getPeoplePhotos(flutterResult: @escaping FlutterResult) {
+        DispatchQueue.global(qos: .userInitiated).async { [weak self] in
+            guard let self = self else { return }
+            
+            let fetchOptions = PHFetchOptions()
+            // 限制处理的照片数量,提高性能
+            fetchOptions.fetchLimit = 1000
+            let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions)
+            
+            // 显示进度
+            DispatchQueue.main.async {
+                print("开始处理人物照片,总数: \(allPhotos.count)")
+            }
+            
+            // 只处理人物照片
+            self.photoClassifier.classifyByPeople(assets: allPhotos) { peopleGroups in
+                // 清理内存
+                self.cleanupMemory()
+                
+                let peopleAssets = Array(peopleGroups.values.flatMap { $0 })
+                
+                // 显示找到的人脸照片数量
+                DispatchQueue.main.async {
+                    print("找到包含人脸的照片: \(peopleAssets.count)")
+                }
+                
+                self.photoClassifier.calculateAssetsSize(peopleAssets) { sizeInfo in
+                    
+                    let resultData = Atomic<[[String: Any]]>([])
+                    
+                    // 分批处理人物组,避免一次性处理太多数据
+                    let processingQueue = DispatchQueue(label: "com.yourapp.peopleProcessing")
+                    let group = DispatchGroup()
+                    
+                    // 处理每个人物组
+                    for (personName, personPhotos) in peopleGroups {
+                        if personPhotos.isEmpty { continue }
+                        
+                        // 限制每组处理的照片数量
+                        let limitedPhotos = Array(personPhotos.prefix(500))
+                        
+                        group.enter()
+                        processingQueue.async {
+                            autoreleasepool {
+                                self.processPhotoGroup(
+                                    assets: limitedPhotos,
+                                    groupName: personName,
+                                    sizeInfo: sizeInfo
+                                ) { groupData in
+                                    if !groupData.isEmpty {
+                                        resultData.mutate { $0.append([
+                                            "group": groupData,
+                                            "type": "people",
+                                            "name": personName
+                                        ]) }
+                                    }
+                                    group.leave()
+                                }
+                            }
+                        }
+                    }
+                    
+                    group.notify(queue: .main) {
+                        // 最终清理内存
+                        self.cleanupMemory()
+                        flutterResult(resultData.value)
+                    }
+                }
+            }
+        }
+    }
+
+    // 获取人物照片
+//    private func getPeoplePhotos(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)
+//            
+//            // 只处理人物照片
+//            self.photoClassifier.classifyByPeople(assets: allPhotos) { peopleGroups in
+//                // 清理内存
+//                self.cleanupMemory()
+//                
+//                let peopleAssets = Array(peopleGroups.values.flatMap { $0 })
+//                self.photoClassifier.calculateAssetsSize(peopleAssets) { sizeInfo in
+//                    
+//                    let resultData = Atomic<[[String: Any]]>([])
+//                    let processingQueue = DispatchQueue(label: "com.yourapp.peopleProcessing")
+//                    let group = DispatchGroup()
+//                    
+//                    // 处理每个人物组
+//                    for (personName, personPhotos) in peopleGroups {
+//                        if personPhotos.isEmpty { continue }
+//                        
+//                        group.enter()
+//                        processingQueue.async {
+//                            autoreleasepool {
+//                                self.processPhotoGroup(
+//                                    assets: personPhotos,
+//                                    groupName: personName,
+//                                    sizeInfo: sizeInfo
+//                                ) { groupData in
+//                                    if !groupData.isEmpty {
+//                                        resultData.mutate { $0.append([
+//                                            "group": groupData,
+//                                            "type": "people",
+//                                            "name": personName
+//                                        ]) }
+//                                    }
+//                                    group.leave()
+//                                }
+//                            }
+//                        }
+//                    }
+//                    
+//                    group.notify(queue: .main) {
+//                        // 最终清理内存
+//                        self.cleanupMemory()
+//                        flutterResult(resultData.value)
+//                    }
+//                }
+//            }
+//        }
+//    }
+
+    // 添加内存清理方法
+    private func cleanupMemory() {
+        // 强制清理内存
+        autoreleasepool {
+            // 触发内存警告,促使系统回收内存
+            UIApplication.shared.performSelector(onMainThread: #selector(UIApplication.beginIgnoringInteractionEvents), with: nil, waitUntilDone: true)
+            UIApplication.shared.performSelector(onMainThread: #selector(UIApplication.endIgnoringInteractionEvents), with: nil, waitUntilDone: true)
+        }
+    }
+
+    // 获取相似照片
+    private func getSimilarPhotos(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)
+            
+            // 只处理相似照片
+            self.photoClassifier.detectSimilarPhotos(
+                assets: allPhotos,
+                progressHandler: { (stage, progress) in
+                    print("Similar Photos Progress: \(stage) - \(progress)")
+                },
+                completion: { similarGroups in
+                    // 清理内存
+                    self.cleanupMemory()
+                    
+                    // 限制处理的组数,避免内存过载
+                    let maxGroupsToProcess = min(50, similarGroups.count)
+                    let limitedGroups = Array(similarGroups.prefix(maxGroupsToProcess))
+                    
+                    let similarAssets = Array(limitedGroups.flatMap { $0 })
+                    self.photoClassifier.calculateAssetsSize(similarAssets) { sizeInfo in
+                        
+                        let resultData = Atomic<[[String: Any]]>([])
+                        
+                        // 分批处理照片组,每批处理少量组
+                        let batchSize = 5
+                        let totalBatches = Int(ceil(Double(limitedGroups.count) / Double(batchSize)))
+                        
+                        self.processSimilarPhotoGroupsInBatches(
+                            groups: limitedGroups,
+                            batchIndex: 0,
+                            totalBatches: totalBatches,
+                            batchSize: batchSize,
+                            sizeInfo: sizeInfo,
+                            resultData: resultData
+                        ) {
+                            // 所有批次处理完成后的回调
+                            // 最终清理内存
+                            self.cleanupMemory()
+                            flutterResult(resultData.value)
+                        }
+                    }
+                }
+            )
+        }
+    }
+    
+    // 添加分批处理照片组的方法
+    private func processSimilarPhotoGroupsInBatches(
+        groups: [[PHAsset]],
+        batchIndex: Int,
+        totalBatches: Int,
+        batchSize: Int,
+        sizeInfo: ClassifyPhoto.PhotoSizeInfo,
+        resultData: Atomic<[[String: Any]]>,
+        completion: @escaping () -> Void
+    ) {
+        // 检查是否处理完所有批次
+        if batchIndex >= totalBatches {
+            completion()
+            return
+        }
+        
+        // 计算当前批次的范围
+        let startIndex = batchIndex * batchSize
+        let endIndex = min(startIndex + batchSize, groups.count)
+        let currentBatchGroups = groups[startIndex..<endIndex]
+        
+        let processingQueue = DispatchQueue(label: "com.yourapp.similarProcessing.batch\(batchIndex)")
+        let group = DispatchGroup()
+        
+        // 处理当前批次的照片组
+        for (index, photoGroup) in currentBatchGroups.enumerated() {
+            if photoGroup.isEmpty { continue }
+            
+            group.enter()
+            processingQueue.async {
+                autoreleasepool {
+                    self.processPhotoGroup(
+                        assets: photoGroup,
+                        groupName: "similar_\(startIndex + index)",
+                        sizeInfo: sizeInfo
+                    ) { groupData in
+                        if !groupData.isEmpty {
+                            resultData.mutate { $0.append([
+                                "group": groupData,
+                                "type": "similar"
+                            ]) }
+                        }
+                        group.leave()
+                    }
+                }
+            }
+        }
+        
+        // 当前批次处理完成后
+        group.notify(queue: .global()) {
+            // 清理内存
+            self.cleanupMemory()
+            
+            // 延迟一小段时间再处理下一批,给系统一些恢复时间
+            DispatchQueue.global().asyncAfter(deadline: .now() + 0.3) {
+                // 递归处理下一批
+                self.processSimilarPhotoGroupsInBatches(
+                    groups: groups,
+                    batchIndex: batchIndex + 1,
+                    totalBatches: totalBatches,
+                    batchSize: batchSize,
+                    sizeInfo: sizeInfo,
+                    resultData: resultData,
+                    completion: completion
+                )
+            }
+        }
+    }
+    
+  private func getPhoto(flutterResult: @escaping FlutterResult) {
       DispatchQueue.global(qos: .userInitiated).async { [weak self] in
-           guard let self = self else { return }
+          guard let self = self else { return }
 
-           let fetchOptions = PHFetchOptions()
-           let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions)
+          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)
-                   }
-               }
-           )
-       }
+          
+          self.photoClassifier.classifyPhotos(
+              assets: allPhotos,
+              progressHandler: { (stage, progress) in
+                  print("Progress: \(stage) - \(progress)")
+              },
+              completion: { result in
+                  // 使用串行队列处理结果,避免同时处理多个组
+                  let processingQueue = DispatchQueue(label: "com.yourapp.photoProcessing")
+                  let resultData = Atomic<[[String: Any]]>([])
+                  
+                  // 创建一个函数来依次处理每个组
+                  func processGroups(index: Int, groups: [(assets: [PHAsset], name: String, type: String, sizeInfo: ClassifyPhoto.PhotoSizeInfo)]) {
+                      // 检查是否处理完所有组
+                      if index >= groups.count {
+                          // 所有组处理完毕,返回结果
+                          DispatchQueue.main.async {
+                              print("Final result count: \(resultData.value.count)")
+                              flutterResult(resultData.value)
+                          }
+                          return
+                      }
+                      
+                      let currentGroup = groups[index]
+                      
+                      // 处理当前组
+                      self.processPhotoGroup(
+                          assets: currentGroup.assets,
+                          groupName: currentGroup.name,
+                          sizeInfo: currentGroup.sizeInfo
+                      ) { groupData in
+                          // 使用自动释放池减少内存占用
+                          autoreleasepool {
+                              if !groupData.isEmpty {
+                                  resultData.mutate { $0.append([
+                                      "group": groupData,
+                                      "type": currentGroup.type,
+                                      currentGroup.type == "location" ? "name" : "" : currentGroup.name
+                                  ].filter { !($0.value is String && $0.value as! String == "") }) }
+                              }
+                              
+                              // 手动触发内存清理
+                              self.cleanupMemory()
+                              
+                              // 延迟一小段时间,让系统有机会回收内存
+                              DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) {
+                                  // 处理下一个组
+                                  processGroups(index: index + 1, groups: groups)
+                              }
+                          }
+                      }
+                  }
+                  
+                  // 准备所有需要处理的组
+                  var allGroups: [(assets: [PHAsset], name: String, type: String, sizeInfo: ClassifyPhoto.PhotoSizeInfo)] = []
+                  
+                  // 添加截图组
+                  if !result.screenshots.isEmpty {
+                      allGroups.append((result.screenshots, "screenshots", "screenshots", result.screenshotsSize))
+                  }
+                  
+                  // 添加相似照片组
+                  for photoGroup in result.similarPhotos {
+                      if !photoGroup.isEmpty {
+                          allGroups.append((photoGroup, "similar", "similar", result.similarPhotosSize))
+                      }
+                  }
+                  
+                  // 添加模糊照片组
+                  if !result.blurryPhotos.isEmpty {
+                      allGroups.append((result.blurryPhotos, "blurry", "blurry", result.blurryPhotosSize))
+                  }
+
+                  // 添加人物照片组
+                  for (personName, personPhotos) in result.people {
+                      if !personPhotos.isEmpty {
+                          allGroups.append((personPhotos, personName, "people", result.peopleSize))
+                      }
+                  }
+                  
+                  // 开始处理第一个组
+                  if allGroups.isEmpty {
+                      DispatchQueue.main.async {
+                          print("No groups to process")
+                          flutterResult([])
+                      }
+                  } else {
+                      processGroups(index: 0, groups: allGroups)
+                  }
+              }
+          )
+          
+//          self.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 cleanupMemory() {
+//        // 清理图像缓存
+//        URLCache.shared.removeAllCachedResponses()
+//        
+//        // 强制进行一次垃圾回收
+//        autoreleasepool {
+//            let _ = [String](repeating: "temp", count: 1)
+//        }
+//        
+//        #if os(iOS)
+//        // 发送低内存警告
+//        UIApplication.shared.perform(Selector(("_performMemoryWarning")))
+//        #endif
+//    }
+    
+    // 添加内存清理方法
+//    private func cleanupMemory() {
+//        // 强制清理内存
+//        autoreleasepool {
+//            // 触发内存警告,促使系统回收内存
+//            UIApplication.shared.performSelector(onMainThread: #selector(UIApplication.beginIgnoringInteractionEvents), with: nil, waitUntilDone: true)
+//            UIApplication.shared.performSelector(onMainThread: #selector(UIApplication.endIgnoringInteractionEvents), with: nil, waitUntilDone: true)
+//        }
+//    }
+    
     // 处理照片组的辅助方法
   private func processPhotoGroup(
     assets: [PHAsset],
@@ -215,20 +651,22 @@ public class ClassifyPhotoPlugin: NSObject, FlutterPlugin {
           photoProcessGroup.enter()
           
           let options = PHContentEditingInputRequestOptions()
-          options.isNetworkAccessAllowed = true
+          options.isNetworkAccessAllowed = false
           
-          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)
+          DispatchQueue.global(qos: .background).async {
+              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)
+                  }
               }
           }
       }
@@ -312,25 +750,89 @@ public class ClassifyPhotoPlugin: NSObject, FlutterPlugin {
 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)
+        // 使用与调用者相同的QoS级别,避免优先级反转
+        DispatchQueue.global(qos: .userInitiated).async {
+            let assets = PHAsset.fetchAssets(withLocalIdentifiers: assetIds, options: nil)
+            
+            // 使用原子操作确保线程安全
+            let totalSize = Atomic<Int64>(0)
+            let processingGroup = DispatchGroup()
+            
+            // 创建一个具有相同QoS的调度队列用于信号量操作
+            let processingQueue = DispatchQueue(label: "com.yourapp.photosize.processing",
+                                               qos: .userInitiated,
+                                               attributes: .concurrent)
+            
+            // 控制并发数量
+            let semaphore = DispatchSemaphore(value: 4)
+            
+            // 分批处理资源
+            let batchSize = 20
+            let totalCount = assets.count
             
-            // 获取 PHAsset 的资源
-            let resources = PHAssetResource.assetResources(for: phAsset)
-            if let resource = resources.first {
-                // 获取文件大小
-                if let unsignedInt64 = resource.value(forKey: "fileSize") as? CLong {
-                    sizes += Int64(unsignedInt64)
+            for batchStart in stride(from: 0, to: totalCount, by: batchSize) {
+                let end = min(batchStart + batchSize, totalCount)
+                
+                processingGroup.enter()
+                // 使用具有明确QoS的队列
+                processingQueue.async {
+                    var batchSize: Int64 = 0
+                    
+                    for i in batchStart..<end {
+                        autoreleasepool {
+                            // 使用带超时的等待,避免无限期阻塞
+                            let waitResult = semaphore.wait(timeout: .now() + 5)
+                            
+                            defer {
+                                // 确保信号量总是被释放
+                                if waitResult != .timedOut {
+                                    semaphore.signal()
+                                }
+                            }
+                            
+                            // 如果等待超时,跳过当前资源
+                            if waitResult == .timedOut {
+                                print("警告: 等待资源超时,跳过资源")
+                                return
+                            }
+                            
+                            let asset = assets.object(at: i)
+                            
+                            // 使用资源管理器获取大小
+                            PHAssetResource.assetResources(for: asset).forEach { resource in
+                                var resourceSize: Int64 = 0
+                                
+                                // 尝试获取文件大小
+                                if let fileSize = resource.value(forKey: "fileSize") as? CLong {
+                                    resourceSize = Int64(fileSize)
+                                }
+                                
+                                batchSize += resourceSize
+                            }
+                        }
+                    }
+                    
+                    // 更新总大小
+                    totalSize.mutate { $0 += batchSize }
+                    processingGroup.leave()
+                }
+            }
+            
+            // 使用带超时的等待,避免无限期阻塞
+            let waitResult = processingGroup.wait(timeout: .now() + 30)
+            
+            // 返回结果到主线程
+            DispatchQueue.main.async {
+                if waitResult == .timedOut {
+                    print("警告: 处理照片大小超时")
+                    completion(FlutterError(code: "TIMEOUT",
+                                           message: "计算照片大小超时",
+                                           details: nil))
+                } else {
+                    completion(totalSize.value)
                 }
             }
         }
-        
-        // 返回文件大小到 Flutter
-        completion(sizes)
     }
 }
 

+ 21 - 0
plugins/classify_photo/lib/classify_photo.dart

@@ -12,6 +12,27 @@ class ClassifyPhoto {
     return ClassifyPhotoPlatform.instance.getPhoto();
   }
 
+  // 新增的分离方法
+  // 获取截图
+  Future<List<Map<String, dynamic>>?> getScreenshots() {
+    return ClassifyPhotoPlatform.instance.getScreenshots();
+  }
+
+  // 获取模糊照片
+  Future<List<Map<String, dynamic>>?> getBlurryPhotos() {
+    return ClassifyPhotoPlatform.instance.getBlurryPhotos();
+  }
+
+  // 获取人物照片
+  Future<List<Map<String, dynamic>>?> getPeoplePhotos() {
+    return ClassifyPhotoPlatform.instance.getPeoplePhotos();
+  }
+
+  // 获取相似照片
+  Future<List<Map<String, dynamic>>?> getSimilarPhotos() {
+    return ClassifyPhotoPlatform.instance.getSimilarPhotos();
+  }
+
   Future<Map<String, int>> getStorageInfo() {
     return ClassifyPhotoPlatform.instance.getStorageInfo();
   }

+ 92 - 0
plugins/classify_photo/lib/classify_photo_method_channel.dart

@@ -126,4 +126,96 @@ class MethodChannelClassifyPhoto extends ClassifyPhotoPlatform {
       return 0;
     }
   }
+
+  @override
+  Future<List<Map<String, dynamic>>?> getScreenshots() async {
+    try {
+      print('Flutter: 调用 getScreenshots 方法');
+      final List<dynamic>? result = await methodChannel.invokeMethod<List<dynamic>>(
+        'getScreenshots',
+      );
+
+      if (result == null) {
+        print('Flutter: 截图结果为空');
+        return null;
+      }
+
+      return result.map((group) => Map<String, dynamic>.from(group)).toList();
+    } on PlatformException catch (e) {
+      print('Flutter: 获取截图异常: ${e.message}');
+      rethrow;
+    } catch (e) {
+      print('Flutter: 获取截图错误: $e');
+      rethrow;
+    }
+  }
+
+  @override
+  Future<List<Map<String, dynamic>>?> getBlurryPhotos() async {
+    try {
+      print('Flutter: 调用 getBlurryPhotos 方法');
+      final List<dynamic>? result = await methodChannel.invokeMethod<List<dynamic>>(
+        'getBlurryPhotos',
+      );
+
+      if (result == null) {
+        print('Flutter: 模糊照片结果为空');
+        return null;
+      }
+
+      return result.map((group) => Map<String, dynamic>.from(group)).toList();
+    } on PlatformException catch (e) {
+      print('Flutter: 获取模糊照片异常: ${e.message}');
+      rethrow;
+    } catch (e) {
+      print('Flutter: 获取模糊照片错误: $e');
+      rethrow;
+    }
+  }
+
+  @override
+  Future<List<Map<String, dynamic>>?> getPeoplePhotos() async {
+    try {
+      print('Flutter: 调用 getPeoplePhotos 方法');
+      final List<dynamic>? result = await methodChannel.invokeMethod<List<dynamic>>(
+        'getPeoplePhotos',
+      );
+
+      if (result == null) {
+        print('Flutter: 人物照片结果为空');
+        return null;
+      }
+
+      return result.map((group) => Map<String, dynamic>.from(group)).toList();
+    } on PlatformException catch (e) {
+      print('Flutter: 获取人物照片异常: ${e.message}');
+      rethrow;
+    } catch (e) {
+      print('Flutter: 获取人物照片错误: $e');
+      rethrow;
+    }
+  }
+
+  @override
+  Future<List<Map<String, dynamic>>?> getSimilarPhotos() async {
+    try {
+      print('Flutter: 调用 getSimilarPhotos 方法');
+      final List<dynamic>? result = await methodChannel.invokeMethod<List<dynamic>>(
+        'getSimilarPhotos',
+      );
+
+      if (result == null) {
+        print('Flutter: 相似照片结果为空');
+        return null;
+      }
+
+      return result.map((group) => Map<String, dynamic>.from(group)).toList();
+    } on PlatformException catch (e) {
+      print('Flutter: 获取相似照片异常: ${e.message}');
+      rethrow;
+    } catch (e) {
+      print('Flutter: 获取相似照片错误: $e');
+      rethrow;
+    }
+  }
 }

+ 17 - 0
plugins/classify_photo/lib/classify_photo_platform_interface.dart

@@ -55,4 +55,21 @@ abstract class ClassifyPhotoPlatform extends PlatformInterface {
   Future<int> calculatePhotoSize(List<String> assetIds) {
     throw UnimplementedError('finishTransaction() has not been implemented.');
   }
+
+  // 添加新的分离方法
+  Future<List<Map<String, dynamic>>?> getScreenshots() {
+    throw UnimplementedError('getScreenshots() has not been implemented.');
+  }
+
+  Future<List<Map<String, dynamic>>?> getBlurryPhotos() {
+    throw UnimplementedError('getBlurryPhotos() has not been implemented.');
+  }
+
+  Future<List<Map<String, dynamic>>?> getPeoplePhotos() {
+    throw UnimplementedError('getPeoplePhotos() has not been implemented.');
+  }
+
+  Future<List<Map<String, dynamic>>?> getSimilarPhotos() {
+    throw UnimplementedError('getSimilarPhotos() has not been implemented.');
+  }
 }

+ 1 - 1
pubspec.yaml

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 # In Windows, build-name is used as the major, minor, and patch parts
 # of the product and file versions while build-number is used as the build suffix.
-version: 1.3.0+27
+version: 1.3.0+28
 
 environment:
   sdk: ^3.6.0

+ 97 - 0
相关文件路径

@@ -0,0 +1,97 @@
+// ... existing code ...
+
+// 2. 比较特征相似度并分组
+group.notify(queue: processingQueue) {
+    progressHandler("正在比较相似度...", 0.6)
+    
+    // 近似度
+    let similarityThreshold: Float = 0.7
+    var similarGroups: [[PHAsset]] = []
+    
+    // 使用并行处理来加速比较
+    let processingGroup = DispatchGroup()
+    let processingQueue = DispatchQueue(label: "com.yourapp.similarity.processing", attributes: .concurrent)
+    let resultsQueue = DispatchQueue(label: "com.yourapp.similarity.results")
+    let semaphore = DispatchSemaphore(value: 8) // 控制并发数量
+    
+    // 创建一个线程安全的数据结构来存储结果
+    var processedIndices = Atomic<Set<Int>>(Set<Int>())
+    var groupResults = Atomic<[Int: [PHAsset]]>([:])
+    
+    // 分批处理,每批处理一部分数据
+    let batchSize = min(100, imageFeatures.count)
+    let batches = Int(ceil(Float(imageFeatures.count) / Float(batchSize)))
+    
+    for batchIndex in 0..<batches {
+        let startIndex = batchIndex * batchSize
+        let endIndex = min(startIndex + batchSize, imageFeatures.count)
+        
+        for i in startIndex..<endIndex {
+            // 检查是否已处理
+            if processedIndices.value.contains(i) { continue }
+            
+            semaphore.wait()
+            processingGroup.enter()
+            
+            processingQueue.async {
+                // 再次检查,因为可能在等待期间被其他线程处理
+                if processedIndices.value.contains(i) {
+                    semaphore.signal()
+                    processingGroup.leave()
+                    return
+                }
+                
+                var similarAssets: [PHAsset] = [imageFeatures[i].asset]
+                processedIndices.mutate { $0.insert(i) }
+                
+                for j in (i + 1)..<imageFeatures.count {
+                    // 检查是否已处理
+                    if processedIndices.value.contains(j) { continue }
+                    
+                    do {
+                        var distance: Float = 0
+                        try imageFeatures[i].feature.computeDistance(&distance, to: imageFeatures[j].feature)
+                        
+                        let similarity = 1 - distance
+                        if similarity >= similarityThreshold {
+                            similarAssets.append(imageFeatures[j].asset)
+                            processedIndices.mutate { $0.insert(j) }
+                        }
+                    } catch {
+                        print("相似度计算失败: \(error)")
+                    }
+                }
+                
+                // 只保存有多个相似图像的组
+                if similarAssets.count > 1 {
+                    resultsQueue.async {
+                        groupResults.mutate { $0[i] = similarAssets }
+                    }
+                }
+                
+                // 更新进度
+                let progress = Float(processedIndices.value.count) / Float(imageFeatures.count)
+                DispatchQueue.main.async {
+                    progressHandler("正在比较相似度...", 0.6 + progress * 0.4)
+                }
+                
+                semaphore.signal()
+                processingGroup.leave()
+            }
+        }
+    }
+    
+    processingGroup.wait()
+    
+    // 整理结果
+    similarGroups = Array(groupResults.value.values)
+    
+    // 按照照片数量降序排序
+    similarGroups.sort { $0.count > $1.count }
+    
+    DispatchQueue.main.async {
+        completion(similarGroups)
+    }
+}
+
+// ... existing code ...