Explorar o código

[feat]设置页和照片分析

Destiny hai 1 ano
pai
achega
aee85354f5
Modificáronse 36 ficheiros con 2033 adicións e 306 borrados
  1. BIN=BIN
      assets/images/icon_setting_agreement.webp
  2. BIN=BIN
      assets/images/icon_setting_privacy.webp
  3. 7 0
      ios/Podfile.lock
  4. 1 0
      ios/Runner/AppDelegate.swift
  5. 2 0
      ios/Runner/Info.plist
  6. 6 0
      ios/Runner/SubscriptionService.swift
  7. 6 1
      lib/data/api/atmob_api.dart
  8. 146 0
      lib/data/api/network_module.dart
  9. 7 0
      lib/data/consts/build_config.dart
  10. 1 1
      lib/data/consts/constants.dart
  11. 28 21
      lib/data/repositories/store_repository.dart
  12. 14 0
      lib/dialog/loading_dialog.dart
  13. 256 0
      lib/module/analysis/analysis_controller.dart
  14. 28 0
      lib/module/analysis/analysis_state.dart
  15. 408 0
      lib/module/analysis/analysis_view.dart
  16. 62 67
      lib/module/more/more_view.dart
  17. 27 8
      lib/module/photo_info/photo_info_controller.dart
  18. 150 109
      lib/module/photo_info/photo_info_view.dart
  19. 3 3
      lib/module/privacy/privacy_controller.dart
  20. 1 37
      lib/module/privacy/privacy_view.dart
  21. 5 0
      lib/module/setting/setting_controller.dart
  22. 154 0
      lib/module/setting/setting_view.dart
  23. 114 0
      lib/module/store/payment_status_manager.dart
  24. 95 11
      lib/module/store/store_controller.dart
  25. 10 2
      lib/router/app_pages.dart
  26. 206 0
      lib/utils/async_util.dart
  27. 37 42
      lib/utils/file_utils.dart
  28. 34 0
      lib/utils/http_handler.dart
  29. 96 4
      lib/utils/image_util.dart
  30. 35 0
      lib/utils/stream_dio_log_interceptor.dart
  31. 34 0
      plugins/classify_photo/ios/Classes/ClassifyPhotoPlugin.swift
  32. 6 0
      plugins/classify_photo/lib/classify_photo.dart
  33. 17 0
      plugins/classify_photo/lib/classify_photo_method_channel.dart
  34. 7 0
      plugins/classify_photo/lib/classify_photo_platform_interface.dart
  35. 24 0
      pubspec.lock
  36. 6 0
      pubspec.yaml

BIN=BIN
assets/images/icon_setting_agreement.webp


BIN=BIN
assets/images/icon_setting_privacy.webp


+ 7 - 0
ios/Podfile.lock

@@ -10,6 +10,9 @@ PODS:
   - disk_space (0.0.1):
     - Flutter
   - Flutter (1.0.0)
+  - in_app_purchase_storekit (0.0.1):
+    - Flutter
+    - FlutterMacOS
   - MMKV (1.3.13):
     - MMKVCore (~> 1.3.13)
   - mmkv_ios (1.0.8):
@@ -39,6 +42,7 @@ DEPENDENCIES:
   - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
   - disk_space (from `.symlinks/plugins/disk_space/ios`)
   - Flutter (from `Flutter`)
+  - in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
   - mmkv_ios (from `.symlinks/plugins/mmkv_ios/ios`)
   - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
   - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
@@ -65,6 +69,8 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/disk_space/ios"
   Flutter:
     :path: Flutter
+  in_app_purchase_storekit:
+    :path: ".symlinks/plugins/in_app_purchase_storekit/darwin"
   mmkv_ios:
     :path: ".symlinks/plugins/mmkv_ios/ios"
   package_info_plus:
@@ -87,6 +93,7 @@ SPEC CHECKSUMS:
   device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
   disk_space: e94d34bbdf77954adfb39e60bde9cc5c7233eda6
   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
+  in_app_purchase_storekit: 8c3b0b3eb1b0f04efbff401c3de6266d4258d433
   MMKV: 5854d45476fc3757bacfa7e13cc0fbcd274ab0e4
   mmkv_ios: 75b9f18f1baf8991985e095192a2b4e35f1e06ea
   MMKVCore: edbad9714bb70344544148f086a4a69061ff31b6

+ 1 - 0
ios/Runner/AppDelegate.swift

@@ -1,4 +1,5 @@
 import Flutter
+import StoreKit
 import UIKit
 
 @main

+ 2 - 0
ios/Runner/Info.plist

@@ -2,6 +2,8 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
+	<key>NSUserTrackingUsageDescription</key>
+	<string>本App需要使用追踪权限用于广告</string>
 	<key>CADisableMinimumFrameDurationOnPhone</key>
 	<true/>
 	<key>CFBundleDevelopmentRegion</key>

+ 6 - 0
ios/Runner/SubscriptionService.swift

@@ -0,0 +1,6 @@
+//
+//  SubscriptionService.swift
+//  Runner
+//
+//  Created by Mac 3 on 2025/1/16.
+//

+ 6 - 1
lib/data/api/atmob_api.dart

@@ -5,10 +5,13 @@ import 'package:clean/data/api/response/order_status_response.dart';
 import 'package:clean/data/api/response/store_index_response.dart';
 import 'package:clean/data/api/response/user_info_response.dart';
 import 'package:dio/dio.dart';
+import 'package:retrofit/error_logger.dart';
 import 'package:retrofit/http.dart';
 
 import '../../base/base_request.dart';
 import '../../base/base_response.dart';
+import '../consts/constants.dart';
+import 'network_module.dart';
 
 part 'atmob_api.g.dart';
 
@@ -31,4 +34,6 @@ abstract class AtmobApi {
   @POST("/project/clean/v1/order/status")
   Future<BaseResponse<OrderStatusResponse>> orderStatus(
       @Body() OrderStatusRequest request);
-}
+}
+
+final atmobApi = AtmobApi(defaultDio, baseUrl: Constants.baseUrl);

+ 146 - 0
lib/data/api/network_module.dart

@@ -0,0 +1,146 @@
+import 'package:dio/dio.dart';
+import 'package:pretty_dio_logger/pretty_dio_logger.dart';
+
+import '../../utils/stream_dio_log_interceptor.dart';
+import '../consts/build_config.dart';
+
+
+class _NetworkModule {
+  static Dio _createDefaultDio() {
+    Dio dio = Dio(BaseOptions(
+
+      /// 发送数据的超时设置。
+      ///
+      /// 超时时会抛出类型为 [DioExceptionType.sendTimeout] 的
+      /// [DioException]。
+      ///
+      /// `null` 或 `Duration.zero` 即不设置超时。
+      //Duration ? sendTimeout;
+
+      /// 接收数据的超时设置。
+      ///
+      /// 这里的超时对应的时间是:
+      ///  - 在建立连接和第一次收到响应数据事件之前的超时。
+      ///  - 每个数据事件传输的间隔时间,而不是接收的总持续时间。
+      ///
+      /// 超时时会抛出类型为 [DioExceptionType.receiveTimeout] 的
+      /// [DioException]。
+      ///
+      /// `null` 或 `Duration.zero` 即不设置超时。
+      //Duration ? receiveTimeout;
+
+      /// 可以在 [Interceptor]、[Transformer] 和
+      /// [Response.requestOptions] 中获取到的自定义对象。
+      //Map<String, dynamic> ? extra;
+
+      /// HTTP 请求头。
+      ///
+      /// 请求头的键是否相等的判断大小写不敏感的。
+      /// 例如:`content-type` 和 `Content-Type` 会视为同样的请求头键。
+      //Map<String, dynamic> ? headers;
+
+      /// 是否保留请求头的大小写。
+      ///
+      /// 默认值为 false。
+      ///
+      /// 该选项在以下场景无效:
+      ///  - XHR 不支持直接处理。
+      ///  - 按照 HTTP/2 的标准,只支持小写请求头键。
+      //bool ? preserveHeaderCase;
+
+      /// 表示 [Dio] 处理请求响应数据的类型。
+      ///
+      /// 默认值为 [ResponseType.json]。
+      /// [Dio] 会在请求响应的 content-type
+      /// 为 [Headers.jsonContentType] 时自动将响应字符串处理为 JSON 对象。
+      ///
+      /// 在以下情况时,分别使用:
+      ///  - `plain` 将数据处理为 `String`;
+      ///  - `bytes` 将数据处理为完整的 bytes。
+      ///  - `stream` 将数据处理为流式返回的二进制数据;
+      //ResponseType? responseType;
+
+      /// 请求的 content-type。
+      ///
+      /// 请求默认的 `content-type` 会由 [ImplyContentTypeInterceptor]
+      /// 根据发送数据的类型推断。它可以通过
+      /// [Interceptors.removeImplyContentTypeInterceptor] 移除。
+      //String? contentType;
+
+      /// 判断当前返回的状态码是否可以视为请求成功。
+      //ValidateStatus? validateStatus;
+
+      /// 是否在请求失败时仍然获取返回数据内容。
+      ///
+      /// 默认为 true。
+      //bool? receiveDataWhenStatusError;
+
+      /// 参考 [HttpClientRequest.followRedirects]。
+      ///
+      /// 默认为 true。
+      //bool? followRedirects;
+
+      /// 当 [followRedirects] 为 true 时,指定的最大重定向次数。
+      /// 如果请求超出了重定向次数上线,会抛出 [RedirectException]。
+      ///
+      /// 默认为 5。
+      //int? maxRedirects;
+
+      /// 参考 [HttpClientRequest.persistentConnection]。
+      ///
+      /// 默认为 true。
+      //bool? persistentConnection;
+
+      /// 对请求内容进行自定义编码转换。
+      ///
+      /// 默认为 [Utf8Encoder]。
+      //RequestEncoder? requestEncoder;
+
+      /// 对请求响应内容进行自定义解码转换。
+      ///
+      /// 默认为 [Utf8Decoder]。
+      //ResponseDecoder? responseDecoder;
+
+      /// 当请求参数以 `x-www-url-encoded` 方式发送时,如何处理集合参数。
+      ///
+      /// 默认为 [ListFormat.multi]。
+      //ListFormat? listFormat;
+    ));
+    dio.interceptors.add(PrettyDioLogger(
+      requestHeader: true,
+      requestBody: true,
+      responseBody: true,
+      responseHeader: true,
+      enabled: BuildConfig.isDebug,
+    ));
+    return dio;
+  }
+
+  static Dio _createStreamDio() {
+    Dio streamDio = Dio(BaseOptions(
+      responseType: ResponseType.stream,
+    ));
+    streamDio.interceptors.add(StreamDioLogInterceptor());
+    return streamDio;
+  }
+
+  static Dio _createFileDio() {
+    Dio dio = Dio(BaseOptions(
+      sendTimeout: const Duration(seconds: 60 * 60),
+    ));
+    dio.interceptors.add(PrettyDioLogger(
+      requestHeader: true,
+      requestBody: true,
+      responseBody: true,
+      responseHeader: true,
+      enabled: BuildConfig.isDebug,
+    ));
+    return dio;
+  }
+}
+
+final defaultDio = _NetworkModule._createDefaultDio();
+
+final streamDio = _NetworkModule._createStreamDio();
+
+final fileDio = _NetworkModule._createFileDio();

+ 7 - 0
lib/data/consts/build_config.dart

@@ -0,0 +1,7 @@
+import 'package:flutter/foundation.dart';
+
+class BuildConfig {
+  BuildConfig._();
+
+  static bool get isDebug => kDebugMode;
+}

+ 1 - 1
lib/data/consts/constants.dart

@@ -1,7 +1,7 @@
 class Constants {
   Constants._();
 
-  static const String env = envProd;
+  static const String env = envDev;
 
   static const String envDev = 'dev';
 

+ 28 - 21
lib/data/repositories/store_repository.dart

@@ -1,4 +1,11 @@
+import 'package:clean/base/base_request.dart';
 
+import '../../utils/http_handler.dart';
+import '../api/atmob_api.dart';
+import '../api/request/order_pay_request.dart';
+import '../api/request/order_status_request.dart';
+import '../api/response/order_pay_response.dart';
+import '../api/response/store_index_response.dart';
 
 class StoreRepository {
   StoreRepository._();
@@ -7,27 +14,27 @@ class StoreRepository {
     return storeRepository;
   }
 
-  // Future<StoreIndexResponse> storeIndex() {
-  //   return atmobApi
-  //       .storeIndex(AppBaseRequest())
-  //       .then(HttpHandler.handle(false));
-  // }
-  //
-  // Future<OrderPayResponse> orderPay(
-  //     int itemId, int payPlatform, int payMethod) {
-  //   return atmobApi
-  //       .orderPay(OrderPayRequest(itemId, payPlatform, payMethod))
-  //       .then(HttpHandler.handle(false));
-  // }
-  //
-  // Future<int> orderStatus(String outTradeNo, {String? receiptData}) {
-  //   return atmobApi
-  //       .orderStatus(OrderStatusRequest(outTradeNo, receiptData))
-  //       .then(HttpHandler.handle(false))
-  //       .then((data) {
-  //     return data.payStatus;
-  //   });
-  // }
+  Future<StoreIndexResponse> storeIndex() {
+    return atmobApi
+        .storeIndex(BaseRequest())
+        .then(HttpHandler.handle(false));
+  }
+
+  Future<OrderPayResponse> orderPay(
+      int itemId, int payPlatform, int payMethod) {
+    return atmobApi
+        .orderPay(OrderPayRequest(itemId, payPlatform, payMethod))
+        .then(HttpHandler.handle(false));
+  }
+
+  Future<int> orderStatus(String outTradeNo, {String? receiptData}) {
+    return atmobApi
+        .orderStatus(OrderStatusRequest(outTradeNo, receiptData))
+        .then(HttpHandler.handle(false))
+        .then((data) {
+      return data.payStatus;
+    });
+  }
 }
 
 final StoreRepository storeRepository = StoreRepository._();

+ 14 - 0
lib/dialog/loading_dialog.dart

@@ -0,0 +1,14 @@
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+
+class LoadingDialog {
+  static void show(String msg, {bool backDismiss = false}) {
+    SmartDialog.showLoading(
+      msg: msg,
+      backType: backDismiss ? SmartBackType.normal : SmartBackType.block,
+    );
+  }
+
+  static void hide() {
+    SmartDialog.dismiss();
+  }
+}

+ 256 - 0
lib/module/analysis/analysis_controller.dart

@@ -0,0 +1,256 @@
+import 'dart:math';
+
+import 'package:clean/base/base_controller.dart';
+import 'package:clean/module/analysis/analysis_state.dart';
+import 'package:clean/utils/expand.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import 'package:get/get_core/src/get_main.dart';
+import 'package:get/get_rx/src/rx_types/rx_types.dart';
+import 'package:permission_handler/permission_handler.dart';
+import 'package:wechat_camera_picker/wechat_camera_picker.dart';
+
+import '../../model/asset_info.dart';
+import '../../utils/file_utils.dart';
+import '../../utils/image_util.dart';
+import '../../utils/toast_util.dart';
+import '../image_picker/image_picker_assets.dart';
+
+class AnalysisController extends BaseController {
+  // 是否为编辑状态
+  RxBool isEdit = false.obs;
+
+  // 使用共享状态
+  RxList<AssetInfo> imageList = AnalysisState.imageList;
+
+  RxMap<String, List<AssetInfo>> assetsByMonth = AnalysisState.assetsByMonth;
+
+  // 获取月份数量
+  int get monthCount => assetsByMonth.length;
+
+  // 获取总图片数量
+  int get totalAssetCount =>
+      assetsByMonth.values.fold(0, (sum, list) => sum + list.length);
+
+  // 存储选中的图片ID
+  final RxSet<String> selectedAssets = <String>{}.obs;
+
+  // 是否全选
+  RxBool isAllSelected = false.obs;
+
+  // 选中图片的总容量(字节)
+  final RxInt selectedTotalSize = 0.obs;
+
+  @override
+  void onInit() {
+    // TODO: implement onInit
+    super.onInit();
+
+    loadAssets();
+  }
+
+  // 加载并分组图片
+  Future<void> loadAssets() async {
+    var newImageList = <AssetInfo>[];
+    newImageList = await FileUtils.getAllAssets(FileType.analysis);
+    if (newImageList.isEmpty) {
+      isEdit.value = false;
+      return;
+    }
+
+    // 清空现有数据
+    assetsByMonth.clear();
+
+    // 按月份分组
+    for (var asset in newImageList) {
+      final monthKey = ImageUtil.getMonthKey(asset.createDateTime);
+      if (!assetsByMonth.containsKey(monthKey)) {
+        assetsByMonth[monthKey] = [];
+      }
+      assetsByMonth[monthKey]!.add(asset);
+      asset.dateTitle = ImageUtil.formatMonthKey(monthKey);
+    }
+
+    newImageList.sort((a, b) => b.createDateTime.compareTo(a.createDateTime));
+
+    AnalysisState.imageList.value = newImageList;
+    AnalysisState.updateMonthlyAssets();
+
+    // 对每个月份内的图片按时间排序(新的在前)
+    assetsByMonth.forEach((key, list) {
+      list.sort((a, b) => b.createDateTime.compareTo(a.createDateTime));
+    });
+
+    // 打印分组结果
+    assetsByMonth.forEach((key, assets) {
+      print('${ImageUtil.formatMonthKey(key)}: ${assets.length} photos');
+    });
+  }
+
+  // 上传按钮点击
+  void uploadBtnClick() {
+    showCupertinoModalPopup(
+      context: Get.context!,
+      builder: (context) {
+        return CupertinoActionSheet(
+          actions: <Widget>[
+            //操作按钮集合
+            CupertinoActionSheetAction(
+              onPressed: () {
+                Navigator.pop(context);
+                openGallery();
+              },
+              child: Text(
+                'Upload from Gallery',
+                style: TextStyle(
+                  color: "#007AFF".color,
+                  fontWeight: FontWeight.w500,
+                  fontSize: 16.sp,
+                ),
+              ),
+            ),
+            CupertinoActionSheetAction(
+              onPressed: () {
+                Navigator.pop(context);
+                openCamera();
+              },
+              child: Text(
+                'Take and Upload',
+                style: TextStyle(
+                  color: "#007AFF".color,
+                  fontWeight: FontWeight.w500,
+                  fontSize: 16.sp,
+                ),
+              ),
+            ),
+          ],
+          cancelButton: CupertinoActionSheetAction(
+            //取消按钮
+            onPressed: () {
+              Navigator.pop(context);
+            },
+            child: Text(
+              'Cancel',
+              style: TextStyle(
+                color: "#007AFF".color,
+                fontWeight: FontWeight.w500,
+                fontSize: 16.sp,
+              ),
+            ),
+          ),
+        );
+      },
+    );
+  }
+
+// 保存并刷新图片列表
+  Future<void> saveAndRefreshAssets(List<AssetEntity> assets) async {
+    for (var asset in assets) {
+      await FileUtils.saveAsset(FileType.analysis, asset);
+    }
+    // 重新加载图片列表
+    loadAssets();
+  }
+
+  // 选择/取消选择图片
+  void toggleSelectAsset(String assetId) {
+    final asset = imageList.firstWhere((asset) => asset.id == assetId);
+
+    if (selectedAssets.contains(assetId)) {
+      selectedAssets.remove(assetId);
+      if (asset.size != null) {
+        selectedTotalSize.value -= asset.size!;
+      }
+    } else {
+      selectedAssets.add(assetId);
+
+      if (asset.size != null) {
+        selectedTotalSize.value += asset.size!;
+      }
+    }
+
+    // 更新全选状态
+    isAllSelected.value = selectedAssets.length == imageList.length;
+  }
+
+  // 全选/取消全选
+  void toggleSelectAll() {
+    if (isAllSelected.value) {
+      selectedAssets.clear();
+      selectedTotalSize.value = 0;
+    } else {
+      selectedAssets.addAll(imageList.map((asset) => asset.id));
+      selectedTotalSize.value = imageList.fold(
+          0, (sum, asset) => sum + (asset.size != null ? asset.size! : 0));
+    }
+    isAllSelected.value = !isAllSelected.value;
+  }
+
+  // 退出编辑模式时清空选择
+  void exitEditMode() {
+    isEdit.value = false;
+    selectedAssets.clear();
+    isAllSelected.value = false;
+    selectedTotalSize.value = 0;
+  }
+
+  // 删除文件
+  void deleteBtnClick() {
+    // 获取要删除的资产
+    final assetsToDelete =
+        imageList.where((asset) => selectedAssets.contains(asset.id)).toList();
+
+    for (var asset in assetsToDelete) {
+      FileUtils.deleteAsset(FileType.analysis, asset.id.substring(0, 36));
+    }
+
+    selectedTotalSize.value = 0;
+    loadAssets();
+  }
+
+  // 格式化文件大小显示
+  String formatFileSize(int bytes) {
+    if (bytes <= 0) return "Delete";
+
+    final units = ['B', 'KB', 'MB', 'GB'];
+    int digitGroups = (log(bytes) / log(1024)).floor();
+
+    if (bytes == 0) {
+      return "Delete";
+    } else {
+      return "Delete(${(bytes / pow(1024, digitGroups)).toStringAsFixed(1)} ${units[digitGroups]})";
+    }
+  }
+
+// 开启图库
+  Future<void> openGallery() async {
+    var status = await Permission.photos.status;
+    if (status == PermissionStatus.granted) {
+      List<AssetEntity> assets = <AssetEntity>[];
+      for (var asset in imageList) {
+        var newAsset = await asset.toAssetEntity();
+        if (newAsset != null) {
+          assets.add(newAsset);
+        }
+      }
+      List<AssetEntity>? pickList = await ImagePickAssets.pick();
+      if (pickList != null && pickList.isNotEmpty) {
+        await saveAndRefreshAssets(pickList);
+      }
+    } else {
+      ToastUtil.show("请先开启权限");
+    }
+  }
+
+// 开启相机
+  Future<void> openCamera() async {
+    final entity = await CameraPicker.pickFromCamera(
+      Get.context!,
+      pickerConfig: const CameraPickerConfig(),
+    );
+    if (entity != null) {
+      await saveAndRefreshAssets([entity]);
+    }
+  }
+}

+ 28 - 0
lib/module/analysis/analysis_state.dart

@@ -0,0 +1,28 @@
+// 创建一个共享状态类
+import 'package:get/get_rx/src/rx_types/rx_types.dart';
+import 'package:intl/intl.dart';
+
+import '../../model/asset_info.dart';
+
+class AnalysisState {
+  static RxList<AssetInfo> imageList = <AssetInfo>[].obs;
+  static RxMap<String, List<AssetInfo>> assetsByMonth = <String, List<AssetInfo>>{}.obs;
+
+  // 删除图片
+  static void removeAsset(String assetId) {
+    imageList.removeWhere((asset) => asset.id == assetId);
+    updateMonthlyAssets();
+  }
+
+  // 更新月份分组
+  static void updateMonthlyAssets() {
+    assetsByMonth.clear();
+    for (var asset in imageList) {
+      final month = DateFormat('yyyy-MM').format(asset.createDateTime);
+      if (!assetsByMonth.containsKey(month)) {
+        assetsByMonth[month] = [];
+      }
+      assetsByMonth[month]!.add(asset);
+    }
+  }
+}

+ 408 - 0
lib/module/analysis/analysis_view.dart

@@ -0,0 +1,408 @@
+
+import 'package:clean/base/base_page.dart';
+import 'package:clean/module/analysis/analysis_controller.dart';
+import 'package:clean/utils/expand.dart';
+import 'package:clean/utils/file_utils.dart';
+import 'package:flutter/Material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import '../../model/asset_info.dart';
+import '../../resource/assets.gen.dart';
+import '../../router/app_pages.dart';
+import '../../utils/image_util.dart';
+import 'analysis_state.dart';
+import 'dart:typed_data';
+
+class AnalysisPage extends BasePage<AnalysisController> {
+  const AnalysisPage({super.key});
+
+  @override
+  bool immersive() {
+    return true;
+  }
+
+  @override
+  bool statusBarDarkFont() => false;
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Stack(
+        children: [
+          buildMain(context),
+          IgnorePointer(
+            child: Assets.images.bgHome.image(
+              width: 360.w,
+            ),
+          ),
+        ],
+      );
+  }
+
+  Widget buildMain(BuildContext context) {
+    return SafeArea(
+      child: Container(
+        padding: EdgeInsets.only(left: 16.w, top: 14.h, right: 16.w),
+        child: Obx(() {
+          return Column(
+            mainAxisAlignment: MainAxisAlignment.start,
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              !controller.isEdit.value
+                  ? Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                children: [
+                  GestureDetector(
+                    onTap: () {
+                      Get.back();
+                    },
+                    child: Assets.images.iconCommonBack
+                        .image(width: 28.w, height: 28.w),
+                  ),
+                  Obx(() {
+                    return Visibility(
+                      visible: AnalysisState.imageList.isNotEmpty,
+                      child: GestureDetector(
+                        onTap: () {
+                          controller.isEdit.value = true;
+                        },
+                        child: Container(
+                          width: 71.w,
+                          height: 30.h,
+                          decoration: BoxDecoration(
+                            color: "#1F2D3F".color,
+                            borderRadius: BorderRadius.all(
+                              Radius.circular(15.h),
+                            ),
+                          ),
+                          child: Center(
+                            child: Text(
+                              "Select",
+                              style: TextStyle(
+                                color: Colors.white,
+                                fontSize: 14.sp,
+                              ),
+                            ),
+                          ),
+                        ),
+                      ),
+                    );
+                  }),
+                ],
+              )
+                  : Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                children: [
+                  GestureDetector(
+                    onTap: () {
+                      controller.isEdit.value = false;
+                    },
+                    child: Container(
+                      width: 71.w,
+                      height: 30.h,
+                      decoration: BoxDecoration(
+                        color: "#1F2D3F".color,
+                        borderRadius: BorderRadius.all(
+                          Radius.circular(15.h),
+                        ),
+                      ),
+                      child: Center(
+                        child: Text(
+                          "Cancel",
+                          style: TextStyle(
+                            color: Colors.white,
+                            fontSize: 14.sp,
+                          ),
+                        ),
+                      ),
+                    ),
+                  ),
+                  Obx(() {
+                    return Visibility(
+                      visible: AnalysisState.imageList.isNotEmpty,
+                      child: GestureDetector(
+                        onTap: () {
+                          controller.toggleSelectAll();
+                        },
+                        child: Text(
+                          "Select All",
+                          style: TextStyle(
+                            color: Colors.white.withOpacity(0.65),
+                            fontSize: 14.sp,
+                          ),
+                        ),
+                      ),
+                    );
+                  }),
+                ],
+              ),
+              SizedBox(
+                height: 12.h,
+              ),
+              Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                children: [
+                  Text(
+                    "Photo Analysis",
+                    style: TextStyle(
+                      color: Colors.white,
+                      fontWeight: FontWeight.w700,
+                      fontSize: 24.sp,
+                    ),
+                  ),
+                ],
+              ),
+              AnalysisState.imageList.isEmpty
+                  ? _buildEmptyPhotoView(context)
+                  : _buildPhotoView(),
+            ],
+          );
+        }),
+      ),
+    );
+  }
+
+  _buildEmptyPhotoView(BuildContext context) {
+    return Center(
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        crossAxisAlignment: CrossAxisAlignment.center,
+        children: [
+          SizedBox(
+            height: 130.h,
+          ),
+          Assets.images.iconPrivacyEmptyImage.image(width: 70.w, height: 70.w),
+          SizedBox(
+            height: 22.h,
+          ),
+          Text(
+            "Upload Photos for Analysis",
+            style: TextStyle(
+              color: Colors.white.withOpacity(0.9),
+              fontWeight: FontWeight.w500,
+              fontSize: 18.sp,
+            ),
+          ),
+          SizedBox(
+            height: 116.h,
+          ),
+          GestureDetector(
+            onTap: () {
+              controller.uploadBtnClick();
+            },
+            child: Container(
+              width: 180.w,
+              height: 48.h,
+              decoration: BoxDecoration(
+                color: "#0279FB".color,
+                borderRadius: BorderRadius.all(
+                  Radius.circular(10.r),
+                ),
+              ),
+              child: Center(
+                child: Text(
+                  "Upload",
+                  style: TextStyle(
+                    color: Colors.white,
+                    fontSize: 16.sp,
+                    fontWeight: FontWeight.w500,
+                  ),
+                ),
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildPhotoView() {
+    return Expanded(
+      child: Column(
+        children: [
+          SizedBox(
+            height: 20.h,
+          ),
+          Expanded(
+            child: ListView.builder(
+              shrinkWrap: true,
+              itemCount: controller.monthCount,
+              itemBuilder: (context, index) {
+                final monthAssets =
+                ImageUtil.getMonthAssets(controller.assetsByMonth, index);
+                return Column(
+                  mainAxisSize: MainAxisSize.min,
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    Text(
+                      ImageUtil.getMonthText(controller.assetsByMonth, index),
+                      style: TextStyle(
+                          fontSize: 16.sp,
+                          fontWeight: FontWeight.w500,
+                          color: Colors.white),
+                    ),
+                    SizedBox(
+                      height: 11.h,
+                    ),
+                    GridView.builder(
+                      shrinkWrap: true,
+                      physics: NeverScrollableScrollPhysics(),
+                      itemCount: monthAssets.length,
+                      itemBuilder: (context, index) {
+                        var asset = monthAssets[index];
+                        return _buildAssetItem(asset);
+                      },
+                      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+                        crossAxisCount: 3, // 每行有 4 列
+                        mainAxisSpacing: 8.w, // 主轴(垂直)上的间距
+                        crossAxisSpacing: 8.w, // 交叉轴(水平)上的间距
+                        childAspectRatio: 1.0, // 子项宽高比
+                      ),
+                    ),
+                    SizedBox(
+                      height: 24.h,
+                    ),
+                  ],
+                );
+              },
+            ),
+          ),
+          !controller.isEdit.value
+              ? GestureDetector(
+            onTap: () {
+              controller.uploadBtnClick();
+            },
+            child: Container(
+              width: 328.w,
+              height: 48.h,
+              decoration: BoxDecoration(
+                color: "#0279FB".color,
+                borderRadius: BorderRadius.all(
+                  Radius.circular(10.r),
+                ),
+              ),
+              child: Center(
+                child: Text(
+                  "Upload",
+                  style: TextStyle(
+                    color: Colors.white,
+                    fontSize: 16.sp,
+                    fontWeight: FontWeight.w500,
+                  ),
+                ),
+              ),
+            ),
+          )
+              : GestureDetector(
+            onTap: () {
+              controller.deleteBtnClick();
+            },
+            child: Container(
+              width: 328.w,
+              height: 48.h,
+              decoration: BoxDecoration(
+                color: "#0279FB".color,
+                borderRadius: BorderRadius.all(
+                  Radius.circular(10.r),
+                ),
+              ),
+              child: Center(
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.center,
+                  children: [
+                    Assets.images.iconPrivacyPhotoDelete
+                        .image(width: 18.w, height: 18.h),
+                    SizedBox(
+                      width: 5.w,
+                    ),
+                    Text(
+                      controller.formatFileSize(
+                          controller.selectedTotalSize.value),
+                      style: TextStyle(
+                        color: Colors.white,
+                        fontSize: 16.sp,
+                        fontWeight: FontWeight.w500,
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+            ),
+          )
+        ],
+      ),
+    );
+  }
+
+  // 构建图片项
+  Widget _buildAssetItem(AssetInfo asset) {
+    return GestureDetector(
+      onTap: () async {
+        // 获取当前资产在列表中的索引
+        final index = AnalysisState.imageList.indexWhere((item) => item.id == asset.id);
+        if (index != -1) { // 确保找到了索引
+          final result = await Get.toNamed(RoutePath.photoInfo, arguments: {
+            "type": "analysis",
+            "list": AnalysisState.imageList,
+            "index": index,
+          });
+
+          // 检查返回结果并刷新
+          if (result != null && result['deleted'] == true) {
+            controller.loadAssets();
+          }
+        }
+      },
+      child: Stack(
+        children: [
+          ClipRRect(
+            borderRadius: BorderRadius.circular(8.r),
+            child: FutureBuilder<Uint8List?>(
+              future: ImageUtil.getImageThumbnail(FileType.analysis, asset),
+              builder: (context, snapshot) {
+                if (snapshot.data != null) {
+                  return Image.memory(
+                    snapshot.data!,
+                    width: double.infinity,
+                    height: double.infinity,
+                    fit: BoxFit.cover,
+                    gaplessPlayback: true,
+                  );
+                } else {
+                  return Container();
+                }
+              },
+            ),
+          ),
+          // 删除按钮
+          Visibility(
+            visible: controller.isEdit.value,
+            child: Positioned(
+              right: 4.w,
+              bottom: 4.h,
+              child: GestureDetector(
+                onTap: () {
+                  controller.toggleSelectAsset(asset.id);
+                },
+                child: Container(
+                  child: controller.selectedAssets.contains(asset.id)
+                      ? Center(
+                    child: Assets.images.iconSelected.image(
+                      width: 16.w,
+                      height: 16.h,
+                    ),
+                  )
+                      : Center(
+                    child: Assets.images.iconUnselected.image(
+                      width: 16.w,
+                      height: 16.h,
+                    ),
+                  ),
+                ),
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 62 - 67
lib/module/more/more_view.dart

@@ -23,80 +23,74 @@ class MorePage extends BaseView<MoreController> {
           mainAxisAlignment: MainAxisAlignment.start,
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
-            Container(
-              child: Text(
-                "CleanPro",
-                style: TextStyle(
-                  color: Colors.white,
-                  fontWeight: FontWeight.w900,
-                  fontSize: 24.sp,
-                ),
+            Text(
+              "CleanPro",
+              style: TextStyle(
+                color: Colors.white,
+                fontWeight: FontWeight.w900,
+                fontSize: 24.sp,
               ),
             ),
-            Expanded(child:
-            SingleChildScrollView(
-              child: Column(
-                mainAxisAlignment: MainAxisAlignment.start,
-                crossAxisAlignment: CrossAxisAlignment.start,
-                children: [
-                  SizedBox(height: 21.h),
-                  _buildStoreCard(),
-                  SizedBox(height: 30.h),
-                  Container(
-                    margin: EdgeInsets.only(left: 2.w),
-                    child: Text(
-                      "More features",
-                      style: TextStyle(
-                        color: Colors.white,
-                        fontWeight: FontWeight.w700,
-                        fontSize: 18.sp,
+            Expanded(
+              child: SingleChildScrollView(
+                child: Column(
+                  mainAxisAlignment: MainAxisAlignment.start,
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    SizedBox(height: 21.h),
+                    _buildStoreCard(),
+                    SizedBox(height: 30.h),
+                    Container(
+                      margin: EdgeInsets.only(left: 2.w),
+                      child: Text(
+                        "More features",
+                        style: TextStyle(
+                          color: Colors.white,
+                          fontWeight: FontWeight.w700,
+                          fontSize: 18.sp,
+                        ),
                       ),
                     ),
-                  ),
-                  SizedBox(height: 18.h),
-                  _buildCustomCard(
-                    "Privacy Space",
-                    Assets.images.iconMorePrivacyBg.image(),
-                    Assets.images.iconMorePrivacy
-                        .image(height: 72.w, width: 72.w),
-                    onTap: () {
+                    SizedBox(height: 18.h),
+                    _buildCustomCard(
+                        "Privacy Space",
+                        Assets.images.iconMorePrivacyBg.image(),
+                        Assets.images.iconMorePrivacy
+                            .image(height: 72.w, width: 72.w), onTap: () {
                       Get.toNamed(RoutePath.privacy);
-                    }
-                  ),
-                  SizedBox(height: 14.h),
-                  _buildCustomCard(
-                    "Photo Analysis",
-                    Assets.images.iconMoreAnalysisBg.image(),
-                    Assets.images.iconMoreAnalysis
-                        .image(height: 72.w, width: 72.w),
-                      onTap: () {
-
-                      }
-                  ),
-                  SizedBox(height: 14.h),
-                  _buildCustomCard(
-                    "Wallpaper",
-                    Assets.images.iconMoreWallpaperBg.image(),
-                    Assets.images.iconMoreWallpaper
-                        .image(height: 72.w, width: 72.w),
+                    }),
+                    SizedBox(height: 14.h),
+                    _buildCustomCard(
+                      "Photo Analysis",
+                      Assets.images.iconMoreAnalysisBg.image(),
+                      Assets.images.iconMoreAnalysis
+                          .image(height: 72.w, width: 72.w),
                       onTap: () {
-
-                      }
-                  ),
-                  SizedBox(height: 14.h),
-                  _buildCustomCard(
-                    "Settings",
-                    Assets.images.iconMoreSettingsBg.image(),
-                    Assets.images.iconMoreSettings
-                        .image(height: 72.w, width: 72.w),
+                        Get.toNamed(RoutePath.analysis);
+                      },
+                    ),
+                    SizedBox(height: 14.h),
+                    _buildCustomCard(
+                        "Wallpaper",
+                        Assets.images.iconMoreWallpaperBg.image(),
+                        Assets.images.iconMoreWallpaper
+                            .image(height: 72.w, width: 72.w),
+                        onTap: () {}),
+                    SizedBox(height: 14.h),
+                    _buildCustomCard(
+                      "Settings",
+                      Assets.images.iconMoreSettingsBg.image(),
+                      Assets.images.iconMoreSettings
+                          .image(height: 72.w, width: 72.w),
                       onTap: () {
-
-                      }
-                  ),
-                  SizedBox(height: 25.h),
-                ],
+                        Get.toNamed(RoutePath.setting);
+                      },
+                    ),
+                    SizedBox(height: 25.h),
+                  ],
+                ),
               ),
-            ),),
+            ),
           ],
         ),
       ),
@@ -151,7 +145,8 @@ class MorePage extends BaseView<MoreController> {
     );
   }
 
-  Widget _buildCustomCard(String title, Image bg, Image icon, {required Function() onTap}) {
+  Widget _buildCustomCard(String title, Image bg, Image icon,
+      {required Function() onTap}) {
     return GestureDetector(
       onTap: onTap,
       child: Stack(

+ 27 - 8
lib/module/photo_info/photo_info_controller.dart

@@ -1,8 +1,10 @@
 import 'dart:math';
 
+import 'package:clean/module/analysis/analysis_state.dart';
 import 'package:clean/module/privacy/privacy_state.dart';
 import 'package:clean/utils/expand.dart';
 import 'package:clean/utils/file_utils.dart';
+import 'package:clean/utils/image_util.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
@@ -12,7 +14,8 @@ import '../../base/base_controller.dart';
 import '../../model/asset_info.dart';
 
 class PhotoInfoController extends BaseController {
-  RxBool isAnalysis = false.obs;
+
+  Rx<FileType> type = FileType.analysis.obs;
 
   // 存储所有图片,按月份分组
   RxList<AssetInfo> imageList = <AssetInfo>[].obs;
@@ -20,6 +23,8 @@ class PhotoInfoController extends BaseController {
   // 当前显示的图片索引
   final RxInt currentImageIndex = 0.obs;
 
+  final RxMap<String, dynamic> photoDetails = <String, dynamic>{}.obs;
+
   @override
   void onInit() {
     // TODO: implement onInit
@@ -28,21 +33,29 @@ class PhotoInfoController extends BaseController {
     if (Get.arguments != null) {
       final args = Get.arguments as Map<String, dynamic>;
       imageList.value = Get.arguments["list"] as List<AssetInfo>;
-      var type = Get.arguments["type"] as String;
-      ;
-      if (type == "privacy") {
-        isAnalysis.value = false;
+      var argType = Get.arguments["type"] as String;
+
+      if (argType == "privacy") {
+        type.value = FileType.privacy;
       } else {
-        isAnalysis.value = true;
+        type.value = FileType.analysis;
       }
 
       // 设置初始索引
       if (args.containsKey('index')) {
         currentImageIndex.value = args['index'] as int;
       }
+
+      loadPhotoInfo();
     }
   }
 
+  Future<void> loadPhotoInfo() async {
+    var assetInfo = imageList[currentImageIndex.value];
+    var asset = await assetInfo.toAssetEntity();
+    photoDetails.value = await ImageUtil.getPhotoDetails(asset!);
+  }
+
   // 切换到下一张图片
   void nextImage() {
     if (currentImageIndex.value < imageList.length - 1) {
@@ -75,14 +88,20 @@ class PhotoInfoController extends BaseController {
             CupertinoActionSheetAction(
               onPressed: () {
                 Navigator.pop(context);
-                FileUtils.deleteAsset(asset.id.substring(0, 36));
+                FileUtils.deleteAsset(type.value, asset.id.substring(0, 36));
 
-                if (!isAnalysis.value) {
+                if (type.value == FileType.privacy) {
                   PrivacyState.removeAsset(asset.id);
                   // 从列表中移除
                   imageList = PrivacyState.imageList;
                 }
 
+                if (type.value == FileType.analysis) {
+                  AnalysisState.removeAsset(asset.id);
+                  // 从列表中移除
+                  imageList = AnalysisState.imageList;
+                }
+
                 // 如果没有图片了,返回上一页
                 if (imageList.isEmpty) {
                   Get.back();

+ 150 - 109
lib/module/photo_info/photo_info_view.dart

@@ -26,76 +26,118 @@ class PhotoInfoPage extends BasePage<PhotoInfoController> {
     return Stack(
       children: [
         SafeArea(
-          child: Column(
-            // mainAxisAlignment: MainAxisAlignment.center,
-            crossAxisAlignment: CrossAxisAlignment.start,
+          child: Stack(
             children: [
-              Container(
-                margin: EdgeInsets.only(left: 16.w, top: 14.h),
-                child: GestureDetector(
-                  onTap: () {
-                    Get.back();
-                  },
-                  child: Assets.images.iconCommonBack
-                      .image(width: 28.w, height: 28.w),
-                ),
-              ),
-              Container(
-                margin: EdgeInsets.only(left: 16.w, top: 12.h),
-                child: Text(
-                  controller.imageList[controller.currentImageIndex.value]
-                      .dateTitle ?? "",
-                  style: TextStyle(
-                    color: Colors.white,
-                    fontWeight: FontWeight.w500,
-                    fontSize: 24.sp,
+              Column(
+                // mainAxisAlignment: MainAxisAlignment.center,
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Container(
+                    margin: EdgeInsets.only(left: 16.w, top: 14.h),
+                    child: GestureDetector(
+                      onTap: () {
+                        Get.back();
+                      },
+                      child: Assets.images.iconCommonBack
+                          .image(width: 28.w, height: 28.w),
+                    ),
                   ),
-                ),
+                  Container(
+                    margin: EdgeInsets.only(left: 16.w, top: 12.h),
+                    child: Obx(() {
+                      if (controller.imageList.isEmpty) {
+                        return SizedBox.shrink();
+                      }
+                      return Text(
+                        controller.imageList[controller.currentImageIndex.value]
+                                .dateTitle ??
+                            "",
+                        style: TextStyle(
+                          color: Colors.white,
+                          fontWeight: FontWeight.w500,
+                          fontSize: 24.sp,
+                        ),
+                      );
+                    }),
+                  ),
+                  SizedBox(
+                    height: 20.h,
+                  ),
+                  _buildImageCarousel(),
+                ],
               ),
-              SizedBox(height: 20.h,),
-              _buildImageCarousel(),
-              Spacer(),
-              Center(
-                child: GestureDetector(
-                  onTap: () {
-                    controller.deleteBtnClick(controller.imageList[controller
-                        .currentImageIndex.value], controller.currentImageIndex.value);
-                  },
-                  child: Container(
-                    width: 328.w,
-                    height: 48.h,
-                    decoration: BoxDecoration(
-                      color: "#0279FB".color,
-                      borderRadius: BorderRadius.all(
-                        Radius.circular(10.r),
-                      ),
-                    ),
-                    child: Center(
-                      child: Row(
-                        mainAxisAlignment: MainAxisAlignment.center,
+              Positioned(
+                left: 0,
+                right: 0,
+                bottom: 0,
+                child: Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    Container(
+                      margin: EdgeInsets.only(left: 18.w),
+                      child: Column(
                         children: [
-                          Assets.images.iconPrivacyPhotoDelete
-                              .image(width: 18.w, height: 18.h),
-                          SizedBox(
-                            width: 5.w,
+                          Text(
+                            "Analysis Results",
+                            style: TextStyle(
+                              color: Colors.white,
+                              fontWeight: FontWeight.w900,
+                              fontSize: 16.sp,
+                            ),
                           ),
-                          Obx(() {
-                            if (controller.imageList.isEmpty) return SizedBox.shrink();
-                            return Text(
-                              controller.formatFileSize(
-                                  controller.imageList[controller
-                                      .currentImageIndex.value].size ?? 0),
-                              style: TextStyle(
-                                color: Colors.white,
-                                fontSize: 16.sp,
-                                fontWeight: FontWeight.w500,
-                              ),
-                            );
-                          }),
                         ],
                       ),
                     ),
-                  ),
+                    Center(
+                      child: GestureDetector(
+                        onTap: () {
+                          controller.deleteBtnClick(
+                              controller.imageList[
+                                  controller.currentImageIndex.value],
+                              controller.currentImageIndex.value);
+                        },
+                        child: Container(
+                          width: 328.w,
+                          height: 48.h,
+                          decoration: BoxDecoration(
+                            color: "#0279FB".color,
+                            borderRadius: BorderRadius.all(
+                              Radius.circular(10.r),
+                            ),
+                          ),
+                          child: Center(
+                            child: Row(
+                              mainAxisAlignment: MainAxisAlignment.center,
+                              children: [
+                                Assets.images.iconPrivacyPhotoDelete
+                                    .image(width: 18.w, height: 18.h),
+                                SizedBox(
+                                  width: 5.w,
+                                ),
+                                Obx(() {
+                                  if (controller.imageList.isEmpty) {
+                                    return SizedBox.shrink();
+                                  }
+                                  return Text(
+                                    controller.formatFileSize(controller
+                                            .imageList[controller
+                                                .currentImageIndex.value]
+                                            .size ??
+                                        0),
+                                    style: TextStyle(
+                                      color: Colors.white,
+                                      fontSize: 16.sp,
+                                      fontWeight: FontWeight.w500,
+                                    ),
+                                  );
+                                }),
+                              ],
+                            ),
+                          ),
+                        ),
+                      ),
+                    ),
+                  ],
                 ),
               ),
             ],
@@ -132,57 +174,56 @@ class PhotoInfoPage extends BasePage<PhotoInfoController> {
           itemCount: controller.imageList.length,
           itemBuilder: (context, index) {
             final asset = controller.imageList[index];
-            return
-              AnimatedPadding(
-                duration: Duration(milliseconds: 300),
-                padding: EdgeInsets.symmetric(
-                  horizontal: 0.w,
-                  vertical:
-                  controller.currentImageIndex.value == index ? 0 : 20.h,
-                ),
-                child:
-                GestureDetector(
-                  // onTap: () => _showImageDetail(asset),
-                  child: Container(
-                    decoration: BoxDecoration(
-                      borderRadius: BorderRadius.circular(12.r),
-                      boxShadow: [
-                        BoxShadow(
-                          color: Colors.black.withOpacity(0.2),
-                          blurRadius: 8,
-                          offset: Offset(0, 4),
-                        ),
-                      ],
-                    ),
-                    child: ClipRRect(
-                      child: FutureBuilder<File?>(
-                        key: ValueKey(asset.id),
-                        future: ImageUtil.getImageFile(asset),
-                        builder: (context, snapshot) {
-                          if (snapshot.hasData && snapshot.data != null) {
-                            return InteractiveViewer(
-                              minScale: 0.5,
-                              maxScale: 3.0,
-                              child: Image.file(
-                                snapshot.data!,
-                                fit: BoxFit.fill,
-                                width: double.infinity,
-                                height: double.infinity,
-                                filterQuality: FilterQuality.high,
-                              ),
-                            );
-                          }
-
-                          return Container(
-                            color: Colors.grey[800],
-                            child: Icon(Icons.error, color: Colors.white60),
-                          );
-                        },
+            return AnimatedPadding(
+              duration: Duration(milliseconds: 300),
+              padding: EdgeInsets.symmetric(
+                horizontal: 0.w,
+                vertical:
+                    controller.currentImageIndex.value == index ? 0 : 20.h,
+              ),
+              child: GestureDetector(
+                // onTap: () => _showImageDetail(asset),
+                child: Container(
+                  decoration: BoxDecoration(
+                    borderRadius: BorderRadius.circular(12.r),
+                    boxShadow: [
+                      BoxShadow(
+                        color: Colors.black.withOpacity(0.2),
+                        blurRadius: 8,
+                        offset: Offset(0, 4),
                       ),
+                    ],
+                  ),
+                  child: ClipRRect(
+                    child: FutureBuilder<File?>(
+                      key: ValueKey(asset.id),
+                      future:
+                          ImageUtil.getImageFile(controller.type.value, asset),
+                      builder: (context, snapshot) {
+                        if (snapshot.hasData && snapshot.data != null) {
+                          return InteractiveViewer(
+                            minScale: 0.5,
+                            maxScale: 3.0,
+                            child: Image.file(
+                              snapshot.data!,
+                              fit: BoxFit.fitHeight,
+                              width: double.infinity,
+                              height: 400,
+                              filterQuality: FilterQuality.high,
+                            ),
+                          );
+                        }
+
+                        return Container(
+                          color: Colors.grey[800],
+                          child: Icon(Icons.error, color: Colors.white60),
+                        );
+                      },
                     ),
                   ),
                 ),
-              );
+              ),
+            );
           },
         ),
       );

+ 3 - 3
lib/module/privacy/privacy_controller.dart

@@ -97,7 +97,7 @@ class PrivacyController extends BaseController {
   // 加载并分组图片
   Future<void> loadAssets() async {
     var newImageList = <AssetInfo>[];
-    newImageList = await FileUtils.getAllAssets();
+    newImageList = await FileUtils.getAllAssets(FileType.privacy);
     if (newImageList.isEmpty) {
       isEdit.value = false;
       return;
@@ -300,7 +300,7 @@ class PrivacyController extends BaseController {
 // 保存并刷新图片列表
   Future<void> saveAndRefreshAssets(List<AssetEntity> assets) async {
     for (var asset in assets) {
-      await FileUtils.saveAsset(asset);
+      await FileUtils.saveAsset(FileType.privacy, asset);
     }
     // 重新加载图片列表
     loadAssets();
@@ -357,7 +357,7 @@ class PrivacyController extends BaseController {
     ).toList();
 
     for (var asset in assetsToDelete) {
-      FileUtils.deleteAsset(asset.id.substring(0, 36));
+      FileUtils.deleteAsset(FileType.privacy, asset.id.substring(0, 36));
     }
 
     selectedTotalSize.value = 0;

+ 1 - 37
lib/module/privacy/privacy_view.dart

@@ -8,14 +8,11 @@ import 'package:clean/utils/expand.dart';
 import 'package:clean/utils/file_utils.dart';
 import 'package:clean/utils/image_util.dart';
 import 'package:flutter/Material.dart';
-import 'package:flutter/cupertino.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
-import 'package:wechat_assets_picker/wechat_assets_picker.dart';
 import '../../base/base_view.dart';
 import '../../dialog/privacy_lock_dialog.dart';
 import '../../resource/assets.gen.dart';
-import '../more/more_controller.dart';
 import 'dart:typed_data';
 
 class PrivacyPage extends BaseView<PrivacyController> {
@@ -591,7 +588,7 @@ class PrivacyPage extends BaseView<PrivacyController> {
           ClipRRect(
             borderRadius: BorderRadius.circular(8.r),
             child: FutureBuilder<Uint8List?>(
-              future: ImageUtil.getImageThumbnail(asset),
+              future: ImageUtil.getImageThumbnail(FileType.privacy, asset),
               builder: (context, snapshot) {
                 if (snapshot.data != null) {
                   return Image.memory(
@@ -639,37 +636,4 @@ class PrivacyPage extends BaseView<PrivacyController> {
       ),
     );
   }
-
-  // 显示图片详情
-  void _showImageDetail(AssetInfo asset) {
-    Get.dialog(
-      Dialog(
-        backgroundColor: Colors.transparent,
-        child: FutureBuilder<File?>(
-          future: ImageUtil.getImageFile(asset),
-          builder: (context, snapshot) {
-            if (snapshot.connectionState == ConnectionState.waiting) {
-              return const Center(child: CircularProgressIndicator());
-            }
-
-            if (snapshot.hasData && snapshot.data != null) {
-              return InteractiveViewer(
-                child: Image.file(
-                  snapshot.data!,
-                  fit: BoxFit.contain,
-                ),
-              );
-            }
-
-            return const Center(
-              child: Text(
-                '加载失败',
-                style: TextStyle(color: Colors.white),
-              ),
-            );
-          },
-        ),
-      ),
-    );
-  }
 }

+ 5 - 0
lib/module/setting/setting_controller.dart

@@ -0,0 +1,5 @@
+import 'package:clean/base/base_controller.dart';
+
+class SettingController extends BaseController {
+
+}

+ 154 - 0
lib/module/setting/setting_view.dart

@@ -0,0 +1,154 @@
+import 'package:clean/base/base_page.dart';
+import 'package:clean/base/base_view.dart';
+import 'package:clean/module/setting/setting_controller.dart';
+import 'package:flutter/Material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+
+import '../../resource/assets.gen.dart';
+
+class SettingPage extends BasePage<SettingController> {
+  const SettingPage({super.key});
+
+  @override
+  bool statusBarDarkFont() {
+    return false;
+  }
+
+  @override
+  bool immersive() {
+    return true;
+  }
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Stack(
+      children: [
+        SafeArea(
+          child: Container(
+            padding: EdgeInsets.only(left: 16.w, top: 14.h, right: 16.w),
+            child: Column(
+              mainAxisAlignment: MainAxisAlignment.start,
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                GestureDetector(
+                  onTap: () {
+                    Get.back();
+                  },
+                  child: Assets.images.iconCommonBack
+                      .image(width: 28.w, height: 28.w),
+                ),
+                SizedBox(
+                  height: 12.h,
+                ),
+                Text(
+                  "Settings",
+                  style: TextStyle(
+                    color: Colors.white,
+                    fontSize: 24.sp,
+                    fontWeight: FontWeight.w700,
+                  ),
+                ),
+                SizedBox(
+                  height: 25.h,
+                ),
+                Container(
+                  height: 124.h,
+                  width: 328.w,
+                  decoration: BoxDecoration(
+                    color: Colors.white.withOpacity(0.12),
+                    borderRadius: BorderRadius.all(
+                      Radius.circular(10.r),
+                    ),
+                  ),
+                  child: Column(
+                    children: [
+                      GestureDetector(
+                        onTap: () {},
+                        child: SizedBox(
+                          height: 62.h,
+                          child: Row(
+                            children: [
+                              SizedBox(
+                                width: 13.w,
+                              ),
+                              Assets.images.iconSettingPrivacy
+                                  .image(width: 20.w, height: 20.w),
+                              SizedBox(
+                                width: 8.w,
+                              ),
+                              Text(
+                                "Privacy Policy",
+                                style: TextStyle(
+                                  color: Colors.white,
+                                  fontSize: 16.sp,
+                                  fontWeight: FontWeight.w500,
+                                ),
+                              ),
+                              Spacer(),
+                              Icon(
+                                Icons.arrow_forward,
+                                color: Colors.white.withOpacity(0.55),
+                              ),
+                              SizedBox(
+                                width: 15.w,
+                              ),
+                            ],
+                          ),
+                        ),
+                      ),
+                      Container(
+                        margin: EdgeInsets.symmetric(horizontal: 16.w),
+                        height: 1,
+                        color: Colors.white.withOpacity(0.06),
+                      ),
+                      GestureDetector(
+                        onTap: () {},
+                        child: SizedBox(
+                          height: 61.h,
+                          child: Row(
+                            children: [
+                              SizedBox(
+                                width: 13.w,
+                              ),
+                              Assets.images.iconSettingAgreement
+                                  .image(width: 20.w, height: 20.w),
+                              SizedBox(
+                                width: 8.w,
+                              ),
+                              Text(
+                                "User Agreement",
+                                style: TextStyle(
+                                  color: Colors.white,
+                                  fontSize: 16.sp,
+                                  fontWeight: FontWeight.w500,
+                                ),
+                              ),
+                              Spacer(),
+                              Icon(
+                                Icons.arrow_forward,
+                                color: Colors.white.withOpacity(0.55),
+                              ),
+                              SizedBox(
+                                width: 15.w,
+                              ),
+                            ],
+                          ),
+                        ),
+                      )
+                    ],
+                  ),
+                )
+              ],
+            ),
+          ),
+        ),
+        IgnorePointer(
+          child: Assets.images.bgHome.image(
+            width: 360.w,
+          ),
+        ),
+      ],
+    );
+  }
+}

+ 114 - 0
lib/module/store/payment_status_manager.dart

@@ -0,0 +1,114 @@
+import 'package:flutter/cupertino.dart';
+import 'package:synchronized/synchronized.dart';
+
+import '../../data/bean/payment_way.dart';
+import '../../data/bean/store_item.dart';
+import '../../data/repositories/store_repository.dart';
+import '../../utils/async_util.dart';
+
+class PaymentStatusManager {
+  PaymentStatusManager._();
+
+  //订单状态
+  //0-查询失败,继续轮询
+  //1-未支付,继续轮询
+  //2-支付成功
+  //3-支付关闭
+  //4-已退款
+  static const int payStatusFail = 0;
+  static const int payStatusUnpaid = 1;
+  static const int payStatusSuccess = 2;
+  static const int payStatusClose = 3;
+  static const int payStatusRefund = 4;
+
+  final Map<String, PaymentStatusCallback> callbackMap = {};
+  final Map<String, CancelableFuture> pollingSubscriptionMap = {};
+  final _lock = Lock();
+
+  void _startCheckPolling(
+      String orderNo, PaymentWay paymentWay, StoreItem storeItemBean,
+      {String? receiptData}) async {
+    await _lock.synchronized(() async {
+      pollingSubscriptionMap[orderNo]?.cancel();
+      debugPrint('开始轮询支付状态: orderNo = $orderNo');
+      CancelableFuture orderFuture = AsyncUtil.retryWithExponentialBackoff(
+              () {
+            return storeRepository
+                .orderStatus(orderNo, receiptData: receiptData)
+                .then((status) {
+              if (status == payStatusSuccess) {
+                return true;
+              } else {
+                throw PaymentStatusException(status);
+              }
+            });
+          },
+          10,
+          predicate: (error) {
+            if (error is PaymentStatusException) {
+              return error.status == payStatusFail ||
+                  error.status == payStatusUnpaid;
+            }
+            return true;
+          });
+      orderFuture.then((data) async {
+        debugPrint('支付成功: orderNo = $orderNo');
+        // accountRepository.refreshUserInfo();
+        await _lock.synchronized(() {
+          callbackMap[orderNo]
+              ?.onPaymentSuccess(orderNo, paymentWay, storeItemBean);
+          callbackMap.remove(orderNo);
+        });
+        reportPaySuccess(storeItemBean.amount, orderNo, storeItemBean.name,
+            paymentWay.payMethod);
+      }).catchError((error) {
+        debugPrint('支付失败: orderNo = $orderNo, error = $error');
+      });
+      pollingSubscriptionMap[orderNo] = orderFuture;
+    });
+  }
+
+  void reportPaySuccess(
+      int price, String orderId, String itemName, int paymentWay) {
+    // EventHandler.reportPay(price, orderId, itemName, paymentWay);
+  }
+
+  void checkPaymentStatus(
+      String orderNo, PaymentWay paymentWay, StoreItem storeItemBean,
+      {String? receiptData}) {
+    // recordKeyInfoToDisk(orderNo, paymentWay, storeItemBean);
+    _startCheckPolling(orderNo, paymentWay, storeItemBean,
+        receiptData: receiptData);
+  }
+
+  void registerPaymentSuccessCallback(
+      String orderNo, PaymentStatusCallback callback) async {
+    await _lock.synchronized(() {
+      callbackMap[orderNo] = callback;
+    });
+  }
+
+  void unregisterPaymentSuccessCallback(PaymentStatusCallback callback) async {
+    await _lock.synchronized(() {
+      callbackMap.removeWhere((key, value) => value == callback);
+    });
+  }
+}
+
+class PaymentStatusException implements Exception {
+  final int status;
+
+  PaymentStatusException(this.status);
+
+  @override
+  String toString() {
+    return '支付状态异常: status = $status';
+  }
+}
+
+abstract class PaymentStatusCallback {
+  void onPaymentSuccess(
+      String orderNo, PaymentWay paymentWay, StoreItem storeItemBean);
+}
+
+final paymentStatusManager = PaymentStatusManager._();

+ 95 - 11
lib/module/store/store_controller.dart

@@ -1,11 +1,24 @@
-import 'package:get/get_rx/src/rx_types/rx_types.dart';
+import 'dart:io';
+
+import 'package:classify_photo/classify_photo.dart';
+import 'package:clean/module/store/payment_status_manager.dart';
+import 'package:flutter/Material.dart';
+import 'package:get/get.dart';
+import 'package:in_app_purchase/in_app_purchase.dart';
 
 import '../../base/base_controller.dart';
+import '../../data/api/response/order_pay_response.dart';
 import '../../data/bean/payment_way.dart';
 import '../../data/bean/store_item.dart';
+import '../../data/consts/constants.dart';
+import '../../data/repositories/store_repository.dart';
+import '../../dialog/loading_dialog.dart';
+import '../../sdk/pay/agile_pay.dart';
+import '../../sdk/pay/applepay/apple_pay_info.dart';
+import '../../sdk/pay/assist/product_type.dart';
 import '../../utils/toast_util.dart';
 
-class StoreController extends BaseController {
+class StoreController extends BaseController implements PaymentStatusCallback {
 
   final RxList<StoreItem> storeItems = <StoreItem>[].obs;
 
@@ -16,17 +29,35 @@ class StoreController extends BaseController {
   final Rxn<PaymentWay> currentSelectedPaymentWay = Rxn<PaymentWay>();
 
   @override
-  void onInit() {
+  Future<void> onInit() async {
     // TODO: implement onInit
-    StoreItem item1 = StoreItem(id: 1, sort: 1, name: "11111", appleGoodsId: "1111", subscribable: 1, amount: 100, originalAmount: 100, auth: "auth", subscriptionMillis: 1, content: "content", priceDesc: "priceDesc", coefficient: 1);
-    StoreItem item2 = StoreItem(id: 2, sort: 1, name: "11111", appleGoodsId: "1111", subscribable: 1, amount: 100, originalAmount: 100, auth: "auth", subscriptionMillis: 1, content: "content", priceDesc: "priceDesc", coefficient: 1);
-    StoreItem item3 = StoreItem(id: 3, sort: 1, name: "11111", appleGoodsId: "1111", subscribable: 1, amount: 100, originalAmount: 100, auth: "auth", subscriptionMillis: 1, content: "content", priceDesc: "priceDesc", coefficient: 1);
+    // StoreItem item1 = StoreItem(id: 1, sort: 1, name: "11111", appleGoodsId: "1111", subscribable: 1, amount: 100, originalAmount: 100, auth: "auth", subscriptionMillis: 1, content: "content", priceDesc: "priceDesc", coefficient: 1);
+    // StoreItem item2 = StoreItem(id: 2, sort: 1, name: "11111", appleGoodsId: "1111", subscribable: 1, amount: 100, originalAmount: 100, auth: "auth", subscriptionMillis: 1, content: "content", priceDesc: "priceDesc", coefficient: 1);
+    // StoreItem item3 = StoreItem(id: 3, sort: 1, name: "11111", appleGoodsId: "1111", subscribable: 1, amount: 100, originalAmount: 100, auth: "auth", subscriptionMillis: 1, content: "content", priceDesc: "priceDesc", coefficient: 1);
+    //
+    // storeItems.add(item1);
+    // storeItems.add(item2);
+    // storeItems.add(item3);
+    //
+    // currentSelectedStoreItem.value = item1;
+
+    initStoreIndexData();
+
+    print(await ClassifyPhoto().checkTrialEligibility());
+  }
 
-    storeItems.add(item1);
-    storeItems.add(item2);
-    storeItems.add(item3);
+  void initStoreIndexData() {
+    storeRepository.storeIndex().then((indexData) {
+      storeItems.clear();
+      storeItems.addAll(indexData.items);
+      currentSelectedStoreItem.value =
+      storeItems.isNotEmpty ? storeItems.first : null;
 
-    currentSelectedStoreItem.value = item1;
+      paymentWays.clear();
+      paymentWays.addAll(indexData.paymentWays);
+      currentSelectedPaymentWay.value =
+      paymentWays.isNotEmpty ? paymentWays.first : null;
+    });
   }
 
   void onBuyClick() async {
@@ -43,6 +74,59 @@ class StoreController extends BaseController {
     }
     int payPlatform = paymentWay.payPlatform;
     int payMethod = paymentWay.payMethod;
-    LoadingDialog.show(StringName.storePayLoading.tr);
+    // LoadingDialog.show(StringName.storePayLoading.tr);
+    try {
+      OrderPayResponse response =
+      await storeRepository.orderPay(storeItem.id, payPlatform, payMethod);
+
+      dynamic payInfo;
+      String outTradeNo = response.outTradeNo;
+      if (payPlatform == PayPlatform.apple) {
+        payInfo = ApplePayInfo(
+            storeItem.appleGoodsId,
+            storeItem.subscribable == 1
+                ? ProductType.nonConsumable
+                : ProductType.consumable,
+            response.appAccountToken);
+      }
+      Future.delayed(const Duration(seconds: 30), () {
+        LoadingDialog.hide();
+      });
+      AgilePay.startPay(payInfo, success: (String? result) {
+        LoadingDialog.show("");
+        checkPaymentStatus(outTradeNo, paymentWay, storeItem,
+            receiptData: result);
+        Future.delayed(const Duration(seconds: 30), () {
+          LoadingDialog.hide();
+        });
+      }, payError: (int error, String? errorMessage) {
+        debugPrint('zk---payError: $error, $errorMessage');
+        // errorPayToast(error);
+        LoadingDialog.hide();
+      }, error: (int errno, String? error) {
+        debugPrint('zk---error: $errno, $error');
+        // errorPayToast(errno);
+        LoadingDialog.hide();
+      });
+    } catch (error) {
+      LoadingDialog.hide();
+      // ToastUtil.showToast(StringName.storePayError.tr);
+    }
+  }
+
+  void checkPaymentStatus(
+      String orderNo, PaymentWay paymentWay, StoreItem storeItemBean,
+      {String? receiptData}) {
+    paymentStatusManager.registerPaymentSuccessCallback(orderNo, this);
+    paymentStatusManager.checkPaymentStatus(orderNo, paymentWay, storeItemBean,
+        receiptData: receiptData);
+  }
+
+  @override
+  void onPaymentSuccess(String orderNo, PaymentWay paymentWay, StoreItem storeItemBean) {
+    // TODO: implement onPaymentSuccess
+    LoadingDialog.hide();
+    ToastUtil.show("Pay success");
+    Get.back();
   }
 }

+ 10 - 2
lib/router/app_pages.dart

@@ -1,3 +1,5 @@
+import 'package:clean/module/analysis/analysis_controller.dart';
+import 'package:clean/module/analysis/analysis_view.dart';
 import 'package:clean/module/home/home_controller.dart';
 import 'package:clean/module/locations_photo/locations_photo_controller.dart';
 import 'package:clean/module/locations_photo/locations_photo_view.dart';
@@ -14,14 +16,14 @@ import 'package:clean/module/people_photo/people_photo_controller.dart';
 import 'package:clean/module/people_photo/people_photo_view.dart';
 import 'package:clean/module/screenshots_blurry/screenshots_controller.dart';
 import 'package:clean/module/screenshots_blurry/screenshots_view.dart';
+import 'package:clean/module/setting/setting_controller.dart';
+import 'package:clean/module/setting/setting_view.dart';
 import 'package:clean/module/similar_photo/similar_photo_controller.dart';
 import 'package:clean/module/similar_photo/similar_photo_view.dart';
 import 'package:clean/module/store/store_controller.dart';
 import 'package:clean/module/store/store_view.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:get/get.dart';
-import 'package:get/get_core/src/get_main.dart';
-import 'package:get/get_instance/src/bindings_interface.dart';
 
 import '../module/main/main_controller.dart';
 import '../module/photo_info/photo_info_view.dart';
@@ -44,6 +46,8 @@ abstract class RoutePath {
   static const store = '/store';
   static const photoPreview = '/photoPreview';
   static const locationsSinglePhoto = '/locationsSinglePhoto';
+  static const setting = '/setting';
+  static const analysis = '/analysis';
   static const photoSelectedPreview = '/photoSelectedPreview';
 }
 
@@ -62,6 +66,8 @@ class AppBinding extends Bindings {
     lazyPut(() => PhotoInfoController());
     lazyPut(() => StoreController());
     lazyPut(() => LocationsSinglePhotoController());
+    lazyPut(() => SettingController());
+    lazyPut(() => AnalysisController());
     lazyPut(() => PhotoSelectedPreviewController());
   }
 
@@ -83,4 +89,6 @@ final generalPages = [
   GetPage(name: RoutePath.store, page: () => StorePage()),
   GetPage(name: RoutePath.locationsSinglePhoto, page: () => LocationsSinglePhotoPage()),
   GetPage(name: RoutePath.photoSelectedPreview, page: () => PhotoSelectedPreviewPage()),
+  GetPage(name: RoutePath.setting, page: () => SettingPage()),
+  GetPage(name: RoutePath.analysis, page: () => AnalysisPage()),
 ];

+ 206 - 0
lib/utils/async_util.dart

@@ -0,0 +1,206 @@
+import 'dart:async';
+
+typedef FutureCallback<T> = Future<T> Function();
+
+typedef IntervalCallback<T> = Future<T> Function(int times);
+
+typedef CancelCallback<T> = void Function();
+
+typedef FutureCompleter<T> = void Function(Completer<T> completer);
+
+typedef Predicate<T> = bool Function(T? value);
+
+class AsyncUtil {
+  AsyncUtil._();
+
+  static CancelableFuture<T> retryWithExponentialBackoff<T>(
+      FutureCallback<T> callback, int maxRetry,
+      {Predicate<dynamic>? predicate}) {
+    const Duration initialInterval = Duration(seconds: 1);
+    int retryCount = 0;
+    Timer? timer;
+
+    void attempt(Completer<T> completer) {
+      callback().then((value) {
+        if (!completer.isCompleted) {
+          completer.complete(value);
+        }
+      }).catchError((error) {
+        if (retryCount < maxRetry && (predicate == null || predicate(error))) {
+          retryCount++;
+          Duration nextInterval = initialInterval * (1 << (retryCount - 1));
+          timer = Timer(nextInterval, () => attempt(completer));
+        } else {
+          if (!completer.isCompleted) {
+            completer.completeError(error);
+          }
+        }
+      });
+    }
+
+    return CancelableFuture<T>((completer) {
+      attempt(completer);
+    }, () {
+      timer?.cancel();
+    });
+  }
+
+  static CancelableFuture<T> retry<T>(
+      FutureCallback<T> callback, Duration interval,
+      {Duration? timeout, int? maxRetry, Predicate<dynamic>? predicate}) {
+    int retryCount = 0;
+    Timer? timer;
+    Timer? timeoutTimer;
+
+    void attempt(Completer<T> completer) {
+      callback().then((value) {
+        if (!completer.isCompleted) {
+          completer.complete(value);
+        }
+      }).catchError((error) {
+        if ((maxRetry == null || maxRetry <= 0 || retryCount < maxRetry) &&
+            (predicate == null || predicate(error))) {
+          retryCount++;
+          timer = Timer(interval, () => attempt(completer));
+        } else {
+          if (!completer.isCompleted) {
+            completer.completeError(error);
+          }
+        }
+      });
+    }
+
+    return CancelableFuture<T>((completer) {
+      if (timeout != null) {
+        timeoutTimer = Timer(timeout, () {
+          if (!completer.isCompleted) {
+            completer.completeError(TimeoutException('Operation timed out'));
+          }
+        });
+      }
+      attempt(completer);
+    }, () {
+      timer?.cancel();
+      timeoutTimer?.cancel();
+    });
+  }
+
+  static CancelableFuture<T> delay<T>(
+      FutureCallback<T> callback, Duration interval) {
+    Timer? timer;
+
+    return CancelableFuture<T>((completer) {
+      timer = Timer(interval, () {
+        callback().then((value) {
+          if (!completer.isCompleted) {
+            completer.complete(value);
+          }
+        }).catchError((error) {
+          if (!completer.isCompleted) {
+            completer.completeError(error);
+          }
+        });
+      });
+    }, () {
+      timer?.cancel();
+    });
+  }
+
+  static StreamController<T> interval<T>(
+      IntervalCallback<T> callback, Duration interval, int times,
+      {Duration? delay}) {
+    Timer? timer;
+    StreamController<T> controller = StreamController<T>(onCancel: () {
+      timer?.cancel();
+    });
+    int count = 0;
+    void tick() {
+      callback(count).then((value) {
+        controller.add(value);
+        count++;
+        if (count < times) {
+          timer = Timer(interval, tick);
+        } else {
+          controller.close();
+        }
+      }).catchError((error) {
+        controller.addError(error);
+        controller.close();
+      });
+    }
+
+    if (delay != null && delay > Duration.zero) {
+      timer = Timer(delay, tick);
+    } else {
+      tick();
+    }
+    return controller;
+  }
+}
+
+abstract interface class Cancelable {
+  void cancel();
+}
+
+class CancelableFuture<T> implements Future<T>, Cancelable {
+  final Completer<T> _completer = Completer<T>();
+
+  final CancelCallback? _cancelable;
+
+  CancelableFuture(FutureCompleter<T> completer, this._cancelable) {
+    completer.call(_completer);
+  }
+
+  @override
+  void cancel() {
+    _cancelable?.call();
+    if (!_completer.isCompleted) {
+      _completer.completeError(CancelledError());
+    }
+  }
+
+  @override
+  Stream<T> asStream() => _completer.future.asStream();
+
+  @override
+  Future<T> catchError(Function onError, {bool Function(Object error)? test}) =>
+      _completer.future.catchError(onError, test: test);
+
+  @override
+  Future<R> then<R>(FutureOr<R> Function(T value) onValue,
+      {Function? onError}) =>
+      _completer.future.then(onValue, onError: onError);
+
+  @override
+  Future<T> timeout(Duration timeLimit, {FutureOr<T> Function()? onTimeout}) =>
+      _completer.future.timeout(timeLimit, onTimeout: onTimeout);
+
+  @override
+  Future<T> whenComplete(FutureOr<void> Function() action) =>
+      _completer.future.whenComplete(action);
+}
+
+class CancelledError extends Error {
+  @override
+  String toString() {
+    return 'Operation was cancelled';
+  }
+}
+
+extension CancelableFutureExtension<T> on Future<T> {
+  CancelableFuture<T> asCancelable(
+      FutureCompleter completer, CancelCallback? cancelable) {
+    CancelableFuture<T> cancelableFuture =
+    CancelableFuture(completer, cancelable);
+    then((value) {
+      if (!cancelableFuture._completer.isCompleted) {
+        cancelableFuture._completer.complete(value);
+      }
+    }).catchError((error) {
+      if (!cancelableFuture._completer.isCompleted) {
+        cancelableFuture._completer.completeError(error);
+      }
+    });
+    return cancelableFuture;
+  }
+}

+ 37 - 42
lib/utils/file_utils.dart

@@ -7,11 +7,21 @@ import 'package:photo_manager/photo_manager.dart';
 
 import '../model/asset_info.dart';
 
+enum FileType {
+  analysis, // 照片分析
+  privacy,  // 隐私空间
+}
+
 class FileUtils {
   /// 获取 AssetEntity 保存目录
-  static Future<String> getAssetPath() async {
+  static Future<String> getAssetPath(FileType type) async {
     final directory = await getApplicationDocumentsDirectory();
-    final path = '${directory.path}/assets';
+    var path = '${directory.path}/assets';
+    if (type == FileType.privacy) {
+      path = "$path/privacy";
+    } else if (type == FileType.analysis) {
+      path = "$path/analysis";
+    }
     final dir = Directory(path);
     if (!dir.existsSync()) {
       dir.createSync(recursive: true);
@@ -20,9 +30,9 @@ class FileUtils {
   }
 
   /// 保存 AssetEntity 到本地
-  static Future<AssetEntity?> saveAsset(AssetEntity asset) async {
+  static Future<AssetEntity?> saveAsset(FileType type, AssetEntity asset) async {
     try {
-      final assetPath = await getAssetPath();
+      final assetPath = await getAssetPath(type);
       final title = asset.id.substring(0, 36);
       final assetFile = File('$assetPath/$title.json');
 
@@ -56,25 +66,10 @@ class FileUtils {
     }
   }
 
-  /// 从本地读取图片数据
-  static Future<Uint8List?> getImageData(int assetId) async {
-    try {
-      final assetPath = await getAssetPath();
-      final imageFile = File('$assetPath/$assetId.jpg');
-      if (await imageFile.exists()) {
-        return await imageFile.readAsBytes();
-      }
-      return null;
-    } catch (e) {
-      print('读取图片数据失败: $e');
-      return null;
-    }
-  }
-
   /// 从本地读取缩略图数据
-  static Future<Uint8List?> getThumbData(String assetId) async {
+  static Future<Uint8List?> getThumbData(FileType type, String assetId) async {
     try {
-      final assetPath = await getAssetPath();
+      final assetPath = await getAssetPath(type);
       final thumbFile = File('$assetPath/${assetId}thumb.jpg');
       if (await thumbFile.exists()) {
         return await thumbFile.readAsBytes();
@@ -87,27 +82,27 @@ class FileUtils {
   }
 
   /// 从本地读取 AssetEntity
-  static Future<AssetEntity?> getAsset(String fileName) async {
-    try {
-      final assetPath = await getAssetPath();
-      final assetFile = File('$assetPath/$fileName.json');
-      if (await assetFile.exists()) {
-        final jsonStr = await assetFile.readAsString();
-        final json = jsonDecode(jsonStr);
-        final assetInfo = AssetInfo.fromJson(json);
-        return await assetInfo.toAssetEntity();
-      }
-      return null;
-    } catch (e) {
-      print('读取 AssetEntity 失败: $e');
-      return null;
-    }
-  }
+  // static Future<AssetEntity?> getAsset(FileType type, String fileName) async {
+  //   try {
+  //     final assetPath = await getAssetPath();
+  //     final assetFile = File('$assetPath/$fileName.json');
+  //     if (await assetFile.exists()) {
+  //       final jsonStr = await assetFile.readAsString();
+  //       final json = jsonDecode(jsonStr);
+  //       final assetInfo = AssetInfo.fromJson(json);
+  //       return await assetInfo.toAssetEntity();
+  //     }
+  //     return null;
+  //   } catch (e) {
+  //     print('读取 AssetEntity 失败: $e');
+  //     return null;
+  //   }
+  // }
 
   /// 获取目录下所有 AssetEntity
-  static Future<List<AssetInfo>> getAllAssets() async {
+  static Future<List<AssetInfo>> getAllAssets(FileType type) async {
     try {
-      final assetPath = await getAssetPath();
+      final assetPath = await getAssetPath(type);
       final assetDir = Directory(assetPath);
       if (!await assetDir.exists()) {
         return [];
@@ -121,7 +116,7 @@ class FileUtils {
           final jsonStr = await entity.readAsString();
           final json = jsonDecode(jsonStr);
           final assetInfo = AssetInfo.fromJson(json);
-          File? file = await ImageUtil.getImageFile(assetInfo);
+          File? file = await ImageUtil.getImageFile(type, assetInfo);
           if (file != null) {
             assetInfo.size = await FileUtils.getFileSize(file);
           }
@@ -137,10 +132,10 @@ class FileUtils {
   }
 
   /// 删除 AssetEntity 文件
-  static Future<bool> deleteAsset(String fileName) async {
+  static Future<bool> deleteAsset(FileType type, String fileName) async {
     try {
 
-      final assetPath = await getAssetPath();
+      final assetPath = await getAssetPath(type);
       final assetFile = File('$assetPath/$fileName.json');
       if (await assetFile.exists()) {
         await assetFile.delete();

+ 34 - 0
lib/utils/http_handler.dart

@@ -0,0 +1,34 @@
+import 'dart:async';
+
+import '../base/base_response.dart';
+
+class HttpHandler {
+  HttpHandler._();
+
+  static FutureOr<T> Function(BaseResponse<T> value) handle<T>(
+      bool allowEmptyData) {
+    return (BaseResponse<T> response) {
+      if (response.code == 0) {
+        if (response.data != null || allowEmptyData) {
+          return response.data == null ? Future.value() : response.data!;
+        } else {
+          throw Exception('data is null');
+        }
+      } else {
+        throw ServerErrorException(response.code, response.message);
+      }
+    };
+  }
+}
+
+class ServerErrorException implements Exception {
+  final int? code;
+  final String? message;
+
+  ServerErrorException(this.code, this.message);
+
+  @override
+  String toString() {
+    return 'ServerErrorException: code: $code, message: $message';
+  }
+}

+ 96 - 4
lib/utils/image_util.dart

@@ -7,6 +7,8 @@ import 'package:wechat_assets_picker/wechat_assets_picker.dart';
 
 import 'file_utils.dart';
 
+import 'package:exif/exif.dart';
+
 class ImageUtil {
 
   // 生成月份 key (用于内部存储)
@@ -47,10 +49,10 @@ class ImageUtil {
   }
 
   // 获取缩略图数据
-  static Future<Uint8List?> getImageThumbnail(AssetInfo asset) async {
+  static Future<Uint8List?> getImageThumbnail(FileType type, AssetInfo asset) async {
     try {
       // 先尝试从本地读取缩略图
-      final localThumb = await FileUtils.getThumbData(asset.id.substring(0, 36));
+      final localThumb = await FileUtils.getThumbData(type, asset.id.substring(0, 36));
       if (localThumb != null) {
         return localThumb;
       }
@@ -64,10 +66,10 @@ class ImageUtil {
   }
 
   // 获取原始图片文件
-  static Future<File?> getImageFile(AssetInfo asset) async {
+  static Future<File?> getImageFile(FileType type, AssetInfo asset) async {
     try {
       // 先尝试从本地读取
-      final assetPath = await FileUtils.getAssetPath();
+      final assetPath = await FileUtils.getAssetPath(type);
       final localFile = File('$assetPath/${asset.id.substring(0, 36)}.jpg');
       if (await localFile.exists()) {
         return localFile;
@@ -80,4 +82,94 @@ class ImageUtil {
       return null;
     }
   }
+
+  static Future<Map<String, dynamic>> getPhotoDetails(AssetEntity asset) async {
+    try {
+      final Map<String, dynamic> details = {};
+
+      // 基本信息
+      details['fileName'] = asset.title;
+      details['createDate'] = asset.createDateTime;
+      details['modifiedDate'] = asset.modifiedDateTime;
+      details['width'] = asset.width;
+      details['height'] = asset.height;
+      details['size'] = asset.size;
+
+      // 获取文件
+      final file = await asset.file;
+      if (file != null) {
+        // 读取 EXIF 数据
+        final bytes = await file.readAsBytes();
+        final exifData = await readExifFromBytes(bytes);
+
+        if (exifData.isNotEmpty) {
+          // 相机信息
+          details['make'] = exifData['Image Make']?.printable;
+          details['model'] = exifData['Image Model']?.printable;
+
+          // 拍摄参数
+          details['aperture'] = exifData['EXIF ApertureValue']?.printable;
+          details['exposureTime'] = exifData['EXIF ExposureTime']?.printable;
+          details['iso'] = exifData['EXIF ISOSpeedRatings']?.printable;
+          details['focalLength'] = exifData['EXIF FocalLength']?.printable;
+
+          // GPS 信息
+          if (exifData.containsKey('GPS GPSLatitude') &&
+              exifData.containsKey('GPS GPSLongitude')) {
+            details['latitude'] = exifData['GPS GPSLatitude']?.printable;
+            details['longitude'] = exifData['GPS GPSLongitude']?.printable;
+          }
+        }
+      }
+
+      print(details);
+      return details;
+    } catch (e) {
+      print('获取照片详情失败: $e');
+      return {};
+    }
+  }
+
+  /// 格式化图片尺寸
+  static String formatResolution(int width, int height) {
+    final megapixels = (width * height) / 1000000.0;
+    if (megapixels >= 1) {
+      return '${megapixels.toStringAsFixed(1)} MP ($width × $height)';
+    } else {
+      return '$width × $height';
+    }
+  }
+
+  /// 格式化文件大小
+  static String formatFileSize(int bytes) {
+    if (bytes < 1024) return '$bytes B';
+    if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
+    if (bytes < 1024 * 1024 * 1024) {
+      return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
+    }
+    return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
+  }
+
+  /// 格式化光圈值
+  static String formatAperture(String? value) {
+    if (value == null) return 'Unknown';
+    return 'f/$value';
+  }
+
+  /// 格式化曝光时间
+  static String formatExposureTime(String? value) {
+    if (value == null) return 'Unknown';
+    // 将分数转换为更易读的格式
+    if (value.contains('/')) {
+      final parts = value.split('/');
+      if (parts.length == 2) {
+        final numerator = int.parse(parts[0]);
+        final denominator = int.parse(parts[1]);
+        if (numerator == 1) {
+          return '1/${denominator}s';
+        }
+      }
+    }
+    return '${value}s';
+  }
 }

+ 35 - 0
lib/utils/stream_dio_log_interceptor.dart

@@ -0,0 +1,35 @@
+import 'package:dio/dio.dart';
+import 'package:flutter/foundation.dart';
+
+class StreamDioLogInterceptor extends Interceptor {
+  @override
+  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
+    if (kDebugMode) {
+      debugPrint('Stream Request: method[${options.method}],'
+          ' url[${options.uri}],'
+          ' headers[${options.headers}]');
+    }
+    handler.next(options);
+  }
+
+  @override
+  void onError(DioException err, ErrorInterceptorHandler handler) {
+    if (kDebugMode) {
+      debugPrint('Stream Error: type[${err.type}],'
+          ' message[${err.message}],'
+          ' error[${err.error}],'
+          ' stackTrace[${err.stackTrace}]');
+    }
+    handler.next(err);
+  }
+
+  @override
+  void onResponse(Response response, ResponseInterceptorHandler handler) {
+    if (kDebugMode) {
+      debugPrint('Stream Response: header[${response.headers}],'
+          ' statusCode[${response.statusCode}],'
+          ' statusMessage[${response.statusMessage}]');
+    }
+    handler.next(response);
+  }
+}

+ 34 - 0
plugins/classify_photo/ios/Classes/ClassifyPhotoPlugin.swift

@@ -1,4 +1,5 @@
 import Flutter
+import StoreKit
 import Photos
 import UIKit
 
@@ -19,6 +20,16 @@ public class ClassifyPhotoPlugin: NSObject, FlutterPlugin {
         self.getPhoto(flutterResult: result)
     case "getStorageInfo":
         getStorageInfo(result: result)
+    case "checkTrialEligibility":
+        if #available(iOS 15.0, *) {
+            Task {
+                let handler = SubscriptionHandler()
+                let isEligible = await handler.checkTrialEligibility()
+                DispatchQueue.main.async {
+                    result(isEligible)
+                }
+            }
+        }
     case "getPlatformVersion":
       result("iOS " + UIDevice.current.systemVersion)
     default:
@@ -219,3 +230,26 @@ public class ClassifyPhotoPlugin: NSObject, FlutterPlugin {
       }
   }
 }
+
+
+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
+        }
+    }
+}

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

@@ -1,4 +1,6 @@
 
+import 'dart:ffi';
+
 import 'classify_photo_platform_interface.dart';
 
 class ClassifyPhoto {
@@ -13,4 +15,8 @@ class ClassifyPhoto {
   Future<Map<String, int>> getStorageInfo() {
     return ClassifyPhotoPlatform.instance.getStorageInfo();
   }
+
+  Future<bool> checkTrialEligibility() {
+    return ClassifyPhotoPlatform.instance.checkTrialEligibility();
+  }
 }

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

@@ -1,3 +1,5 @@
+import 'dart:ffi';
+
 import 'package:flutter/foundation.dart';
 import 'package:flutter/services.dart';
 
@@ -16,6 +18,21 @@ class MethodChannelClassifyPhoto extends ClassifyPhotoPlatform {
   }
 
   @override
+  Future<bool> checkTrialEligibility() async {
+    try {
+      // 调用原生方法并处理可能的空值
+      final result = await methodChannel.invokeMethod<bool>('checkTrialEligibility');
+      return result ?? false; // 如果结果为 null,返回 false
+    } on PlatformException catch (e) {
+      print('检查试用资格失败: ${e.message}');
+      return false; // 发生错误时返回 false
+    } catch (e) {
+      print('未知错误: $e');
+      return false; // 其他错误情况返回 false
+    }
+  }
+
+  @override
   Future<List<Map<String, dynamic>>?> getPhoto() async {
     try {
       print('Flutter: 调用 getPhoto 方法');

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

@@ -1,3 +1,5 @@
+import 'dart:ffi';
+
 import 'package:plugin_platform_interface/plugin_platform_interface.dart';
 
 import 'classify_photo_method_channel.dart';
@@ -35,4 +37,9 @@ abstract class ClassifyPhotoPlatform extends PlatformInterface {
   Future<Map<String, int>> getStorageInfo() {
     throw UnimplementedError('getStorageInfo() has not been implemented.');
   }
+
+  // 检查是否试用
+  Future<bool> checkTrialEligibility() {
+    throw UnimplementedError('checkTrialEligibility() has not been implemented.');
+  }
 }

+ 24 - 0
pubspec.lock

@@ -325,6 +325,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "0.2.1"
+  exif:
+    dependency: "direct main"
+    description:
+      name: exif
+      sha256: a7980fdb3b7ffcd0b035e5b8a5e1eef7cadfe90ea6a4e85ebb62f87b96c7a172
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.3.0"
   extended_image:
     dependency: transitive
     description:
@@ -1046,6 +1054,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.10.0"
+  sprintf:
+    dependency: transitive
+    description:
+      name: sprintf
+      sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "7.0.0"
   stack_trace:
     dependency: transitive
     description:
@@ -1094,6 +1110,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "28.1.38"
+  synchronized:
+    dependency: "direct main"
+    description:
+      name: synchronized
+      sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.3.0+3"
   term_glyph:
     dependency: transitive
     description:

+ 6 - 0
pubspec.yaml

@@ -70,6 +70,9 @@ dependencies:
   # 弹窗
   flutter_smart_dialog: ^4.9.8
 
+  #并发
+  synchronized: ^3.3.0+2
+
   #上、下拉刷新
   pull_to_refresh: ^2.0.0
 
@@ -82,6 +85,9 @@ dependencies:
   #tabbar
   convex_bottom_bar: ^3.2.0
 
+  #获取图片数据
+  exif: ^3.3.0
+
   intl: ^0.19.0
 
   # The following adds the Cupertino Icons font to your application.