Просмотр исходного кода

[feat]完成商店页和照片分析页

Destiny 1 год назад
Родитель
Сommit
b943d4f156
33 измененных файлов с 814 добавлено и 73 удалено
  1. 7 0
      ios/Podfile.lock
  2. 5 0
      lib/data/api/atmob_api.dart
  3. 21 0
      lib/data/api/request/subscription_resume_request.dart
  4. 1 1
      lib/data/bean/member_info.dart
  5. 0 3
      lib/data/bean/store_item.dart
  6. 7 1
      lib/data/consts/constants.dart
  7. 12 4
      lib/data/repositories/store_repository.dart
  8. 53 0
      lib/data/repositories/user_repository.dart
  9. 7 2
      lib/model/asset_info.dart
  10. 7 0
      lib/module/analysis/analysis_controller.dart
  11. 61 0
      lib/module/browser/browser_controller.dart
  12. 65 0
      lib/module/browser/browser_view.dart
  13. 6 1
      lib/module/home/home_controller.dart
  14. 3 0
      lib/module/main/main_controller.dart
  15. 38 2
      lib/module/photo_info/photo_info_controller.dart
  16. 149 18
      lib/module/photo_info/photo_info_view.dart
  17. 2 0
      lib/module/privacy/privacy_controller.dart
  18. 8 2
      lib/module/setting/setting_view.dart
  19. 46 12
      lib/module/store/store_controller.dart
  20. 16 3
      lib/module/store/store_view.dart
  21. 5 0
      lib/router/app_pages.dart
  22. 27 4
      lib/sdk/pay/agile_pay.dart
  23. 1 1
      lib/sdk/pay/applepay/apple_pay.dart
  24. 55 9
      lib/sdk/pay/assist/apple_or_google_pay.dart
  25. 4 0
      lib/sdk/pay/listener/i_agile_pay.dart
  26. 14 1
      lib/utils/file_utils.dart
  27. 32 8
      lib/utils/image_util.dart
  28. 76 1
      plugins/classify_photo/ios/Classes/ClassifyPhotoPlugin.swift
  29. 9 0
      plugins/classify_photo/lib/classify_photo.dart
  30. 26 0
      plugins/classify_photo/lib/classify_photo_method_channel.dart
  31. 5 0
      plugins/classify_photo/lib/classify_photo_platform_interface.dart
  32. 40 0
      pubspec.lock
  33. 6 0
      pubspec.yaml

+ 7 - 0
ios/Podfile.lock

@@ -34,6 +34,9 @@ PODS:
   - video_player_avfoundation (0.0.1):
     - Flutter
     - FlutterMacOS
+  - webview_flutter_wkwebview (0.0.1):
+    - Flutter
+    - FlutterMacOS
 
 DEPENDENCIES:
   - app_tracking_transparency (from `.symlinks/plugins/app_tracking_transparency/ios`)
@@ -50,6 +53,7 @@ DEPENDENCIES:
   - photo_manager (from `.symlinks/plugins/photo_manager/ios`)
   - sensors_plus (from `.symlinks/plugins/sensors_plus/ios`)
   - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
+  - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
 
 SPEC REPOS:
   trunk:
@@ -85,6 +89,8 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/sensors_plus/ios"
   video_player_avfoundation:
     :path: ".symlinks/plugins/video_player_avfoundation/darwin"
+  webview_flutter_wkwebview:
+    :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
 
 SPEC CHECKSUMS:
   app_tracking_transparency: e169b653478da7bb15a6c61209015378ca73e375
@@ -103,6 +109,7 @@ SPEC CHECKSUMS:
   photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
   sensors_plus: 7229095999f30740798f0eeef5cd120357a8f4f2
   video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
+  webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4
 
 PODFILE CHECKSUM: ad7614c9873991faf6165fbd46dcff66209d9783
 

+ 5 - 0
lib/data/api/atmob_api.dart

@@ -1,5 +1,6 @@
 import 'package:clean/data/api/request/order_pay_request.dart';
 import 'package:clean/data/api/request/order_status_request.dart';
+import 'package:clean/data/api/request/subscription_resume_request.dart';
 import 'package:clean/data/api/response/order_pay_response.dart';
 import 'package:clean/data/api/response/order_status_response.dart';
 import 'package:clean/data/api/response/store_index_response.dart';
@@ -34,6 +35,10 @@ abstract class AtmobApi {
   @POST("/project/clean/v1/order/status")
   Future<BaseResponse<OrderStatusResponse>> orderStatus(
       @Body() OrderStatusRequest request);
+
+  @POST("/project/clean/v1/member/subscription/resume")
+  Future<BaseResponse> resume(
+      @Body() SubscriptionResumeRequest request);
 }
 
 final atmobApi = AtmobApi(defaultDio, baseUrl: Constants.baseUrl);

+ 21 - 0
lib/data/api/request/subscription_resume_request.dart

@@ -0,0 +1,21 @@
+import 'package:clean/base/base_request.dart';
+import 'package:json_annotation/json_annotation.dart';
+
+part 'subscription_resume_request.g.dart';
+
+@JsonSerializable()
+class SubscriptionResumeRequest extends BaseRequest {
+  @JsonKey(name: 'receiptData')
+  String receiptData;
+
+  @JsonKey(name: 'payPlatform')
+  int payPlatform;
+
+  @JsonKey(name: 'payMethod')
+  int payMethod;
+
+  SubscriptionResumeRequest(this.payPlatform, this.payMethod, this.receiptData);
+
+  @override
+  Map<String, dynamic> toJson() => _$SubscriptionResumeRequestToJson(this);
+}

+ 1 - 1
lib/data/bean/member_info.dart

@@ -8,7 +8,7 @@ class MemberInfo {
   bool isMember;
 
   @JsonKey(name: 'endTimestamp')
-  int endTimestamp;
+  int? endTimestamp;
 
   MemberInfo(this.isMember, this.endTimestamp);
 

+ 0 - 3
lib/data/bean/store_item.dart

@@ -25,8 +25,6 @@ class StoreItem {
   late String auth;
   @JsonKey(name: "subscriptionMillis")
   late int subscriptionMillis;
-  @JsonKey(name: "content")
-  late String content;
   @JsonKey(name: "priceDesc")
   late String priceDesc;
   @JsonKey(name: "coefficient")
@@ -42,7 +40,6 @@ class StoreItem {
         required this.originalAmount,
         required this.auth,
         required this.subscriptionMillis,
-        required this.content,
         required this.priceDesc,
         required this.coefficient});
 

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

@@ -9,12 +9,18 @@ class Constants {
 
   static const String envProd = 'prod';
 
-  static const String _devBaseUrl = "http://192.168.10.230:8880";
+  static const String _devBaseUrl = "https://ws00.south.takin.cc";
 
   static const String _testBaseUrl = "http://42.193.245.11";
 
   static const String _prodBaseUrl = "https://project-api.atmob.com";
 
+  static const String privacyPolicy =
+      "https://cdn.supercleaner.club/static/cleanpro/clean_pro_privacy.html";
+
+  static const String userAgreement =
+      "https://cdn.supercleaner.club/static/cleanpro/clean_pro_terms.html";
+
   static String baseUrl = getBaseUrl();
 
   static const String appDefaultChannel = "Android";

+ 12 - 4
lib/data/repositories/store_repository.dart

@@ -1,4 +1,5 @@
 import 'package:clean/base/base_request.dart';
+import 'package:clean/data/api/request/subscription_resume_request.dart';
 
 import '../../utils/http_handler.dart';
 import '../api/atmob_api.dart';
@@ -14,10 +15,17 @@ class StoreRepository {
     return storeRepository;
   }
 
-  Future<StoreIndexResponse> storeIndex() {
+  Future<int> resume(
+      int payPlatform, int payMethod, String receiptData) {
     return atmobApi
-        .storeIndex(BaseRequest())
-        .then(HttpHandler.handle(false));
+        .resume(SubscriptionResumeRequest(payPlatform, payMethod, receiptData))
+        .then(HttpHandler.handle(false)).then((data) {
+      return data;
+    });
+  }
+
+  Future<StoreIndexResponse> storeIndex() {
+    return atmobApi.storeIndex(BaseRequest()).then(HttpHandler.handle(false));
   }
 
   Future<OrderPayResponse> orderPay(
@@ -37,4 +45,4 @@ class StoreRepository {
   }
 }
 
-final StoreRepository storeRepository = StoreRepository._();
+final StoreRepository storeRepository = StoreRepository._();

+ 53 - 0
lib/data/repositories/user_repository.dart

@@ -0,0 +1,53 @@
+import 'package:get/get_rx/src/rx_types/rx_types.dart';
+
+import '../../base/base_request.dart';
+import '../../utils/http_handler.dart';
+import '../api/atmob_api.dart';
+import '../api/response/user_info_response.dart';
+
+class UserRepository {
+
+  UserRepository._();
+
+  Rxn<UserInfoResponse> get userInfo => _userInfo;
+
+  final Rxn<UserInfoResponse> _userInfo = Rxn<UserInfoResponse>();
+
+  Future<UserInfoResponse> getUserInfo() {
+    return atmobApi
+        .userInfo(BaseRequest())
+        .then(HttpHandler.handle(false))
+        .then((response) {
+      _userInfo.value = response;
+      return response;
+    });
+  }
+
+  // 检测是否为会员
+  bool isVip() {
+    // 检查用户信息和会员信息是否存在
+    if (userInfo.value?.memberInfo == null) {
+      return false;
+    }
+
+    final memberInfo = userInfo.value!.memberInfo!;
+
+    // 检查是否是会员
+    if (!memberInfo.isMember) {
+      return false;
+    }
+
+    // 检查会员是否过期
+    if (memberInfo.endTimestamp == null) {
+      return false;
+    }
+
+    // 将时间戳转换为DateTime并比较
+    final endTime = DateTime.fromMillisecondsSinceEpoch(memberInfo.endTimestamp ?? 0);
+    final now = DateTime.now();
+
+    return now.isBefore(endTime);
+  }
+}
+
+final userRepository = UserRepository._();

+ 7 - 2
lib/model/asset_info.dart

@@ -18,6 +18,7 @@ class AssetInfo {
   final String? localPath;
   final String? filePath;
   final String? thumbFilePath;
+  final String? originalPath;
   String? dateTitle;
   int? size;
 
@@ -36,13 +37,14 @@ class AssetInfo {
     this.relativePath,
     this.filePath,
     this.thumbFilePath,
+    this.originalPath,
     this.size,
     this.dateTitle,
   });
 
   // 从 AssetEntity 创建
-  factory AssetInfo.fromAssetEntity(
-      AssetEntity entity, String filePath, String thumbFilePath) {
+  factory AssetInfo.fromAssetEntity(AssetEntity entity, String filePath,
+      String thumbFilePath, String originalPath) {
     return AssetInfo(
       id: entity.id,
       width: entity.width,
@@ -57,6 +59,7 @@ class AssetInfo {
       relativePath: entity.relativePath,
       filePath: filePath,
       thumbFilePath: thumbFilePath,
+      originalPath: originalPath,
     );
   }
 
@@ -76,6 +79,7 @@ class AssetInfo {
         'localPath': localPath,
         'file': filePath,
         'thumbFile': thumbFilePath,
+        'originalPath': originalPath,
       };
 
   // 从 JSON 创建
@@ -95,6 +99,7 @@ class AssetInfo {
         localPath: json['localPath'] as String?,
         filePath: json['filePath'] as String?,
         thumbFilePath: json['thumbFilePath'] as String?,
+        originalPath: json['originalPath'] as String?,
       );
 
   // MARK: - To Do

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

@@ -1,5 +1,6 @@
 import 'dart:math';
 
+import 'package:classify_photo/classify_photo.dart';
 import 'package:clean/base/base_controller.dart';
 import 'package:clean/module/analysis/analysis_state.dart';
 import 'package:clean/utils/expand.dart';
@@ -54,6 +55,8 @@ class AnalysisController extends BaseController {
   Future<void> loadAssets() async {
     var newImageList = <AssetInfo>[];
     newImageList = await FileUtils.getAllAssets(FileType.analysis);
+    AnalysisState.imageList.value = newImageList;
+    AnalysisState.updateMonthlyAssets();
     if (newImageList.isEmpty) {
       isEdit.value = false;
       return;
@@ -237,6 +240,10 @@ class AnalysisController extends BaseController {
       List<AssetEntity>? pickList = await ImagePickAssets.pick();
       if (pickList != null && pickList.isNotEmpty) {
         await saveAndRefreshAssets(pickList);
+        final asset = pickList.first;
+        final file = await asset.originFile;
+        // final exifInfo = await ClassifyPhoto().getPhotoExif(file!.path);
+        // print(exifInfo);
       }
     } else {
       ToastUtil.show("请先开启权限");

+ 61 - 0
lib/module/browser/browser_controller.dart

@@ -0,0 +1,61 @@
+
+import 'package:flutter/Material.dart';
+import 'package:get/get.dart';
+import '../../base/base_controller.dart';
+import 'package:webview_flutter/webview_flutter.dart';
+
+class BrowserController extends BaseController {
+  String url = (Get.arguments is String) ? (Get.arguments as String) : '';
+
+  final WebViewController webViewController = WebViewController();
+
+  final title = ''.obs;
+
+  @override
+  void onInit() {
+    super.onInit();
+    _initWebSetting();
+  }
+
+  @override
+  void onReady() {
+    super.onReady();
+    _loadUrl();
+  }
+
+  void _initWebSetting() {
+    webViewController.setJavaScriptMode(JavaScriptMode.unrestricted);
+    webViewController.setBackgroundColor(Colors.white);
+    webViewController.setNavigationDelegate(
+      NavigationDelegate(
+        onPageFinished: (String url) {
+          _getTitle();
+        },
+      ),
+    );
+  }
+
+  void _loadUrl() {
+    if (url.isEmpty) {
+      return;
+    }
+    webViewController.loadRequest(Uri.parse(url));
+  }
+
+  Future<bool> handleBack() async {
+    if (await webViewController.canGoBack()) {
+      webViewController.goBack();
+      return false;
+    }
+    return true;
+  }
+
+  void _getTitle() async {
+    await Future.delayed(const Duration(milliseconds: 500));
+    webViewController.getTitle().then((title) {
+      if (title != null) {
+        this.title.value = title;
+      }
+    });
+  }
+}

+ 65 - 0
lib/module/browser/browser_view.dart

@@ -0,0 +1,65 @@
+import 'package:flutter/Material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import 'package:get/get_core/src/get_main.dart';
+import 'package:webview_flutter/webview_flutter.dart';
+
+import '../../base/base_page.dart';
+import '../../resource/assets.gen.dart';
+import '../../router/app_pages.dart';
+import 'browser_controller.dart';
+
+class BrowserPage extends BasePage<BrowserController> {
+  const BrowserPage({super.key});
+
+  static start(String url) {
+    Get.toNamed(RoutePath.browser, arguments: url);
+  }
+
+  @override
+  bool immersive() {
+    // TODO: implement immersive
+    return true;
+  }
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return WillPopScope(
+      onWillPop: () async {
+        return await controller.handleBack();
+      },
+      child: Scaffold(
+        backgroundColor: Colors.transparent,
+        appBar: AppBar(
+          systemOverlayStyle: SystemUiOverlayStyle.light,
+          backgroundColor: Colors.transparent,
+          title: Obx(
+            () => Text(
+              controller.title.value,
+              style: TextStyle(
+                fontSize: 17.sp,
+                color: Colors.white,
+              ),
+            ),
+          ),
+          leading: IconButton(
+            icon: SizedBox(
+                width: 24.w,
+                height: 24.w,
+                child: Assets.images.iconCommonBack.image()),
+            // Custom icon
+            onPressed: () {
+              Get.back();
+            },
+          ),
+        ),
+        body: _buildContentView(),
+      ),
+    );
+  }
+
+  Widget _buildContentView() {
+    return WebViewWidget(controller: controller.webViewController);
+  }
+}

+ 6 - 1
lib/module/home/home_controller.dart

@@ -2,6 +2,7 @@ import 'dart:io';
 
 import 'package:classify_photo/classify_photo.dart';
 import 'package:clean/base/base_controller.dart';
+import 'package:clean/data/repositories/user_repository.dart';
 import 'package:clean/module/image_picker/image_picker_util.dart';
 import 'package:clean/module/locations_photo/locations_photo_view.dart';
 import 'package:clean/module/people_photo/people_photo_view.dart';
@@ -16,6 +17,8 @@ import 'dart:typed_data';
 
 import 'package:wechat_assets_picker/wechat_assets_picker.dart';
 
+import '../../data/api/response/user_info_response.dart';
+
 class HomeController extends BaseController {
 
   Rx<double> totalSpace = 500.0.obs;
@@ -53,6 +56,8 @@ class HomeController extends BaseController {
   // 是否扫描完成
   RxBool isScanned = false.obs;
 
+  UserInfoResponse? get userInfo => userRepository.userInfo.value;
+
   @override
   Future<void> onInit() async {
     // TODO: implement onInit
@@ -67,8 +72,8 @@ class HomeController extends BaseController {
       ToastUtil.show("请先开启相册权限");
     }
   }
-  Future<void> loadPhotosFromDirectory() async {
 
+  Future<void> loadPhotosFromDirectory() async {
 
     if(ImagePickerUtil.peoplePhotos.isEmpty||ImagePickerUtil.similarPhotos.isEmpty||ImagePickerUtil.locationPhotos.isEmpty||ImagePickerUtil.screenshotPhotos.isEmpty){
       try {

+ 3 - 0
lib/module/main/main_controller.dart

@@ -2,6 +2,7 @@ import 'dart:io';
 import 'dart:ui';
 import 'dart:typed_data' as typed;
 
+import 'package:clean/data/repositories/user_repository.dart';
 import 'package:get/get.dart';
 import 'package:wechat_assets_picker/wechat_assets_picker.dart';
 
@@ -17,6 +18,8 @@ class MainController extends BaseController {
   @override
   void onInit() {
     super.onInit();
+
+    userRepository.getUserInfo();
   }
 
   void changeIndex(int index) {

+ 38 - 2
lib/module/photo_info/photo_info_controller.dart

@@ -1,6 +1,8 @@
 import 'dart:math';
 
+import 'package:classify_photo/classify_photo.dart';
 import 'package:clean/module/analysis/analysis_state.dart';
+import 'package:clean/module/image_picker/image_picker_util.dart';
 import 'package:clean/module/privacy/privacy_state.dart';
 import 'package:clean/utils/expand.dart';
 import 'package:clean/utils/file_utils.dart';
@@ -25,6 +27,20 @@ class PhotoInfoController extends BaseController {
 
   final RxMap<String, dynamic> photoDetails = <String, dynamic>{}.obs;
 
+  RxString createTime = "".obs;
+
+  RxString fileName = "".obs;
+
+  RxString model = "".obs;
+
+  RxString focalLength = "".obs;
+
+  RxString size = "".obs;
+
+  RxString iso = "".obs;
+
+  RxString address = "".obs;
+
   @override
   void onInit() {
     // TODO: implement onInit
@@ -51,16 +67,35 @@ class PhotoInfoController extends BaseController {
   }
 
   Future<void> loadPhotoInfo() async {
+
+    createTime.value = "";
+    fileName.value = "";
+    model.value = "";
+    focalLength.value = "";
+    size.value = "";
+    iso.value = "";
+
     var assetInfo = imageList[currentImageIndex.value];
-    var asset = await assetInfo.toAssetEntity();
-    photoDetails.value = await ImageUtil.getPhotoDetails(asset!);
+    photoDetails.value = await ImageUtil.getPhotoDetails(assetInfo);
+    if (photoDetails.isNotEmpty) {
+      createTime.value = photoDetails["createDate"] as String;
+      fileName.value = photoDetails["fileName"] as String;
+      model.value = photoDetails["model"] as String;
+      focalLength.value =
+      "${photoDetails["focalLength"] as String}mm f/${photoDetails["aperture"] as String}";
+      var imageSize = ImagePickerUtil.formatFileSize(photoDetails["size"], decimals: 1);
+      size.value = "${photoDetails["width"] as int} X ${photoDetails["height"] as int} $imageSize";
+      iso.value = "ISO${photoDetails["iso"]} ${photoDetails["exposureTime"]}s";
+    }
   }
 
+
   // 切换到下一张图片
   void nextImage() {
     if (currentImageIndex.value < imageList.length - 1) {
       currentImageIndex.value++;
     }
+    loadPhotoInfo();
   }
 
   // 切换到上一张图片
@@ -68,6 +103,7 @@ class PhotoInfoController extends BaseController {
     if (currentImageIndex.value > 0) {
       currentImageIndex.value--;
     }
+    loadPhotoInfo();
   }
 
   void deleteBtnClick(AssetInfo asset, int index) {

+ 149 - 18
lib/module/photo_info/photo_info_view.dart

@@ -50,7 +50,7 @@ class PhotoInfoPage extends BasePage<PhotoInfoController> {
                       }
                       return Text(
                         controller.imageList[controller.currentImageIndex.value]
-                                .dateTitle ??
+                            .dateTitle ??
                             "",
                         style: TextStyle(
                           color: Colors.white,
@@ -75,25 +75,155 @@ class PhotoInfoPage extends BasePage<PhotoInfoController> {
                   children: [
                     Container(
                       margin: EdgeInsets.only(left: 18.w),
-                      child: Column(
-                        children: [
-                          Text(
-                            "Analysis Results",
-                            style: TextStyle(
-                              color: Colors.white,
-                              fontWeight: FontWeight.w900,
-                              fontSize: 16.sp,
+                      child: Obx(() {
+                        return Column(
+                          crossAxisAlignment: CrossAxisAlignment.start,
+                          children: [
+                            Text(
+                              "Analysis Results",
+                              style: TextStyle(
+                                color: Colors.white,
+                                fontWeight: FontWeight.w900,
+                                fontSize: 16.sp,
+                              ),
                             ),
-                          ),
-                        ],
-                      ),
+                            Visibility(
+                              visible: controller.createTime.value.isNotEmpty,
+                              child: Column(
+                                children: [
+                                  SizedBox(
+                                    height: 20.h,
+                                  ),
+                                  Text(
+                                    controller.createTime.value,
+                                    style: TextStyle(
+                                      color: Colors.white,
+                                      fontWeight: FontWeight.w500,
+                                      fontSize: 13.sp,
+                                    ),
+                                  ),
+                                ],
+                              ),
+                            ),
+                            Visibility(
+                              visible: controller.fileName.value.isNotEmpty,
+                              child: Column(
+                                children: [
+                                  SizedBox(
+                                    height: 12.h,
+                                  ),
+                                  Text(
+                                    controller.fileName.value,
+                                    style: TextStyle(
+                                      color: Colors.white,
+                                      fontWeight: FontWeight.w500,
+                                      fontSize: 13.sp,
+                                    ),
+                                  ),
+                                ],
+                              ),
+                            ),
+                            Visibility(
+                              visible: controller.model.value.isNotEmpty,
+                              child: Column(
+                                children: [
+                                  SizedBox(
+                                    height: 12.h,
+                                  ),
+                                  Text(
+                                    controller.model.value,
+                                    style: TextStyle(
+                                      color: Colors.white,
+                                      fontWeight: FontWeight.w500,
+                                      fontSize: 13.sp,
+                                    ),
+                                  ),
+                                ],
+                              ),
+                            ),
+                            Visibility(
+                              visible: controller.focalLength.value.isNotEmpty,
+                              child: Column(
+                                children: [
+                                  SizedBox(
+                                    height: 12.h,
+                                  ),
+                                  Text(
+                                    controller.focalLength.value,
+                                    style: TextStyle(
+                                      color: Colors.white,
+                                      fontWeight: FontWeight.w500,
+                                      fontSize: 13.sp,
+                                    ),
+                                  ),
+                                ],
+                              ),
+                            ),
+                            Visibility(
+                              visible: controller.size.value.isNotEmpty,
+                              child: Column(
+                                children: [
+                                  SizedBox(
+                                    height: 12.h,
+                                  ),
+                                  Text(
+                                    controller.size.value,
+                                    style: TextStyle(
+                                      color: Colors.white,
+                                      fontWeight: FontWeight.w500,
+                                      fontSize: 13.sp,
+                                    ),
+                                  ),
+                                ],
+                              ),
+                            ),
+                            Visibility(
+                              visible: controller.iso.value.isNotEmpty,
+                              child: Column(
+                                children: [
+                                  SizedBox(
+                                    height: 12.h,
+                                  ),
+                                  Text(
+                                    controller.iso.value,
+                                    style: TextStyle(
+                                      color: Colors.white,
+                                      fontWeight: FontWeight.w500,
+                                      fontSize: 13.sp,
+                                    ),
+                                  ),
+                                ],
+                              ),
+                            ),
+                            Visibility(
+                              visible: controller.address.value.isNotEmpty,
+                              child: Column(
+                                children: [
+                                  SizedBox(
+                                    height: 12.h,
+                                  ),
+                                  Text(
+                                    controller.address.value,
+                                    style: TextStyle(
+                                      color: Colors.white,
+                                      fontWeight: FontWeight.w500,
+                                      fontSize: 13.sp,
+                                    ),
+                                  ),
+                                ],
+                              ),
+                            ),
+                            SizedBox(height: 32.h,),
+                          ],
+                        );
+                      }),
                     ),
                     Center(
                       child: GestureDetector(
                         onTap: () {
                           controller.deleteBtnClick(
                               controller.imageList[
-                                  controller.currentImageIndex.value],
+                              controller.currentImageIndex.value],
                               controller.currentImageIndex.value);
                         },
                         child: Container(
@@ -120,9 +250,9 @@ class PhotoInfoPage extends BasePage<PhotoInfoController> {
                                   }
                                   return Text(
                                     controller.formatFileSize(controller
-                                            .imageList[controller
-                                                .currentImageIndex.value]
-                                            .size ??
+                                        .imageList[controller
+                                        .currentImageIndex.value]
+                                        .size ??
                                         0),
                                     style: TextStyle(
                                       color: Colors.white,
@@ -169,6 +299,7 @@ class PhotoInfoPage extends BasePage<PhotoInfoController> {
           ),
           onPageChanged: (index) {
             controller.currentImageIndex.value = index;
+            controller.loadPhotoInfo();
             PhotoManager.clearFileCache();
           },
           itemCount: controller.imageList.length,
@@ -179,7 +310,7 @@ class PhotoInfoPage extends BasePage<PhotoInfoController> {
               padding: EdgeInsets.symmetric(
                 horizontal: 0.w,
                 vertical:
-                    controller.currentImageIndex.value == index ? 0 : 20.h,
+                controller.currentImageIndex.value == index ? 0 : 20.h,
               ),
               child: GestureDetector(
                 // onTap: () => _showImageDetail(asset),
@@ -198,7 +329,7 @@ class PhotoInfoPage extends BasePage<PhotoInfoController> {
                     child: FutureBuilder<File?>(
                       key: ValueKey(asset.id),
                       future:
-                          ImageUtil.getImageFile(controller.type.value, asset),
+                      ImageUtil.getImageFile(controller.type.value, asset),
                       builder: (context, snapshot) {
                         if (snapshot.hasData && snapshot.data != null) {
                           return InteractiveViewer(

+ 2 - 0
lib/module/privacy/privacy_controller.dart

@@ -98,6 +98,8 @@ class PrivacyController extends BaseController {
   Future<void> loadAssets() async {
     var newImageList = <AssetInfo>[];
     newImageList = await FileUtils.getAllAssets(FileType.privacy);
+    PrivacyState.imageList.value = newImageList;
+    PrivacyState.updateMonthlyAssets();
     if (newImageList.isEmpty) {
       isEdit.value = false;
       return;

+ 8 - 2
lib/module/setting/setting_view.dart

@@ -1,11 +1,13 @@
 import 'package:clean/base/base_page.dart';
 import 'package:clean/base/base_view.dart';
+import 'package:clean/data/consts/constants.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';
+import '../browser/browser_view.dart';
 
 class SettingPage extends BasePage<SettingController> {
   const SettingPage({super.key});
@@ -64,7 +66,9 @@ class SettingPage extends BasePage<SettingController> {
                   child: Column(
                     children: [
                       GestureDetector(
-                        onTap: () {},
+                        onTap: () {
+                          BrowserPage.start(Constants.privacyPolicy);
+                        },
                         child: SizedBox(
                           height: 62.h,
                           child: Row(
@@ -103,7 +107,9 @@ class SettingPage extends BasePage<SettingController> {
                         color: Colors.white.withOpacity(0.06),
                       ),
                       GestureDetector(
-                        onTap: () {},
+                        onTap: () {
+                          BrowserPage.start(Constants.userAgreement);
+                        },
                         child: SizedBox(
                           height: 61.h,
                           child: Row(

+ 46 - 12
lib/module/store/store_controller.dart

@@ -1,6 +1,7 @@
 import 'dart:io';
 
 import 'package:classify_photo/classify_photo.dart';
+import 'package:clean/data/repositories/user_repository.dart';
 import 'package:clean/module/store/payment_status_manager.dart';
 import 'package:flutter/Material.dart';
 import 'package:get/get.dart';
@@ -30,20 +31,11 @@ class StoreController extends BaseController implements PaymentStatusCallback {
 
   @override
   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);
-    //
-    // storeItems.add(item1);
-    // storeItems.add(item2);
-    // storeItems.add(item3);
-    //
-    // currentSelectedStoreItem.value = item1;
 
     initStoreIndexData();
-
     print(await ClassifyPhoto().checkTrialEligibility());
+
+    // AgilePay.check();
   }
 
   void initStoreIndexData() {
@@ -60,6 +52,32 @@ class StoreController extends BaseController implements PaymentStatusCallback {
     });
   }
 
+  Future<void> onRestoreClick() async {
+    PaymentWay? paymentWay = currentSelectedPaymentWay.value;
+    if (paymentWay == null) {
+      // ToastUtil.showToast(StringName.storeChoicePayment.tr);
+      return;
+    }
+    int payPlatform = paymentWay.payPlatform;
+    int payMethod = paymentWay.payMethod;
+
+    AgilePay.restore(success: (String? result) {
+      LoadingDialog.show("");
+      checkRestoreStatus(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();
+    });
+  }
+
   void onBuyClick() async {
 
     StoreItem? storeItem = currentSelectedStoreItem.value;
@@ -74,7 +92,7 @@ class StoreController extends BaseController implements PaymentStatusCallback {
     }
     int payPlatform = paymentWay.payPlatform;
     int payMethod = paymentWay.payMethod;
-    // LoadingDialog.show(StringName.storePayLoading.tr);
+    LoadingDialog.show("");
     try {
       OrderPayResponse response =
       await storeRepository.orderPay(storeItem.id, payPlatform, payMethod);
@@ -114,6 +132,21 @@ class StoreController extends BaseController implements PaymentStatusCallback {
     }
   }
 
+  // 检查恢复订阅结果
+  void checkRestoreStatus(String? receiptData) {
+    PaymentWay? paymentWay = currentSelectedPaymentWay.value;
+    if (paymentWay == null) {
+      // ToastUtil.showToast(StringName.storeChoicePayment.tr);
+      return;
+    }
+    if (receiptData == null) {
+      return;
+    }
+    int payPlatform = paymentWay.payPlatform;
+    int payMethod = paymentWay.payMethod;
+    var code = storeRepository.resume(payPlatform, payMethod, receiptData);
+  }
+
   void checkPaymentStatus(
       String orderNo, PaymentWay paymentWay, StoreItem storeItemBean,
       {String? receiptData}) {
@@ -127,6 +160,7 @@ class StoreController extends BaseController implements PaymentStatusCallback {
     // TODO: implement onPaymentSuccess
     LoadingDialog.hide();
     ToastUtil.show("Pay success");
+    userRepository.getUserInfo();
     Get.back();
   }
 }

+ 16 - 3
lib/module/store/store_view.dart

@@ -5,6 +5,7 @@ import 'package:clean/utils/expand.dart';
 import 'package:flutter/Material.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
+import 'package:intl/intl.dart';
 
 import '../../resource/assets.gen.dart';
 
@@ -52,7 +53,7 @@ class StorePage extends BasePage<StoreController> {
                       margin: EdgeInsets.only(right: 16.w, top: 17.h),
                       child: GestureDetector(
                         onTap: () {
-                          Get.back();
+                          // controller.onRestoreClick();
                         },
                         child: Text(
                           'Restore',
@@ -186,6 +187,9 @@ class StorePage extends BasePage<StoreController> {
                       ),
                     ),
                     child: GestureDetector(
+                      onTap: () {
+                        controller.onBuyClick();
+                      },
                       child: Center(
                         child: Text(
                           "CONTINUE",
@@ -268,6 +272,15 @@ class StorePage extends BasePage<StoreController> {
     required StoreItem item,
   }) {
     bool isSelected = controller.currentSelectedStoreItem.value?.id == item.id;
+    final formatter = NumberFormat.currency(
+      symbol: '\$',
+      decimalDigits: 2,
+    );
+    var amount = formatter.format(item.amount / 100);
+
+    var content = "{totalPrice}, only {dailyPrice} per day";
+    content = content.replaceAll("{totalPrice}", "\$${item.amount / 100}");
+    content = content.replaceAll("{dailyPrice}", "\$${(item.amount / 100 / item.coefficient).toStringAsFixed(2)}");
     return Obx(() {
       return Container(
         height: 80.h,
@@ -336,7 +349,7 @@ class StorePage extends BasePage<StoreController> {
                                     ),
                                   ),
                                   Text(
-                                    item.name,
+                                    content,
                                     style: TextStyle(
                                       color: Colors.white60,
                                       fontSize: 12.sp,
@@ -345,7 +358,7 @@ class StorePage extends BasePage<StoreController> {
                                 ],
                               ),
                               Text(
-                                item.priceDesc,
+                                amount,
                                 style: TextStyle(
                                   color: Colors.white,
                                   fontSize: 16.sp,

+ 5 - 0
lib/router/app_pages.dart

@@ -25,6 +25,8 @@ import 'package:clean/module/store/store_view.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:get/get.dart';
 
+import '../module/browser/browser_controller.dart';
+import '../module/browser/browser_view.dart';
 import '../module/main/main_controller.dart';
 import '../module/photo_info/photo_info_view.dart';
 import '../module/privacy/privacy_controller.dart';
@@ -36,6 +38,7 @@ abstract class AppPage {
 }
 
 abstract class RoutePath {
+  static const browser = '/browser';
   static const mainTab = '/mainTab';
   static const privacy = '/privacy';
   static const peoplePhoto = '/peoplePhoto';
@@ -54,6 +57,7 @@ abstract class RoutePath {
 class AppBinding extends Bindings {
   @override
   void dependencies() {
+    lazyPut(() => BrowserController());
     lazyPut(() => MainController());
     lazyPut(() => HomeController());
     lazyPut(() => PrivacyController());
@@ -78,6 +82,7 @@ class AppBinding extends Bindings {
 }
 
 final generalPages = [
+  GetPage(name: RoutePath.browser, page: () => const BrowserPage()),
   GetPage(name: RoutePath.mainTab, page: () => MainTabPage()),
   GetPage(name: RoutePath.privacy, page: () => PrivacyPage()),
   GetPage(name: RoutePath.peoplePhoto, page: () => PeoplePhotoPage()),

+ 27 - 4
lib/sdk/pay/agile_pay.dart

@@ -7,11 +7,35 @@ import 'listener/i_agile_pay.dart';
 class AgilePay {
   static IAgilePay? realPay;
 
+  static void check() {
+    IAgilePay? iAgilePay;
+    iAgilePay = ApplePay();
+    realPay = iAgilePay;
+    iAgilePay.check();
+  }
+
+  static void restore(
+      {required AgilePaySuccess success,
+      required AgilePayError payError,
+      required AgileError error,
+      AgilePayBefore? before}) {
+    IAgilePay? iAgilePay;
+    iAgilePay = ApplePay();
+    realPay = iAgilePay;
+
+    iAgilePay.setPayListener(AgilePayStateImpl(
+        paySuccessListener: success,
+        payErrorListener: payError,
+        errorListener: error,
+        payBeforeListener: before));
+    iAgilePay.restore();
+  }
+
   static void startPay(dynamic payInfo,
       {required AgilePaySuccess success,
-        required AgilePayError payError,
-        required AgileError error,
-        AgilePayBefore? before}) {
+      required AgilePayError payError,
+      required AgileError error,
+      AgilePayBefore? before}) {
     IAgilePay? iAgilePay;
 
     if (payInfo is ApplePayInfo) {
@@ -31,5 +55,4 @@ class AgilePay {
           AgilePayCode.getMessageByCode(AgilePayCode.payCodeNotSupport));
     }
   }
-
 }

+ 1 - 1
lib/sdk/pay/applepay/apple_pay.dart

@@ -3,5 +3,5 @@ import '../assist/apple_or_google_pay.dart';
 import '../listener/i_agile_pay.dart';
 
 class ApplePay extends AppleOrGooglePay implements IAgilePay {
-  ApplePay(super.payInfo);
+  ApplePay([super.payInfo]);
 }

+ 55 - 9
lib/sdk/pay/assist/apple_or_google_pay.dart

@@ -9,7 +9,7 @@ import 'agile_pay_state_info.dart';
 import 'apple_or_google_pay_info.dart';
 
 abstract class AppleOrGooglePay extends AgilePayStateInfo {
-  final AppleOrGooglePayInfo _payInfo;
+  final AppleOrGooglePayInfo? _payInfo;
 
   late final StreamSubscription<List<PurchaseDetails>>
   _purchaseUpdatedSubscription;
@@ -17,7 +17,9 @@ abstract class AppleOrGooglePay extends AgilePayStateInfo {
 
   final Duration timeout = const Duration(seconds: 10);
 
-  AppleOrGooglePay(this._payInfo) {
+  bool isCheck = false;
+
+  AppleOrGooglePay([this._payInfo]) {
     _iap = InAppPurchase.instance;
     _purchaseUpdatedSubscription =
         _iap.purchaseStream.listen((purchaseDetailsList) {
@@ -27,6 +29,48 @@ abstract class AppleOrGooglePay extends AgilePayStateInfo {
         }, onError: (error) {});
   }
 
+  Future<void> check() async {
+    isCheck = true;
+
+    Stream purchaseStream = InAppPurchase.instance.purchaseStream;
+    purchaseStream.listen((purchaseDetails) async {
+      for (var purchaseDetail in purchaseDetails) {
+        if (purchaseDetails.status == PurchaseStatus.pending) {
+          verifyPendingPurchase(purchaseDetails);
+        } else {
+
+          if (purchaseDetails.status == PurchaseStatus.error) {
+
+          } else if (purchaseDetails.status == PurchaseStatus.purchased) {
+
+          } else if (purchaseDetails.status == PurchaseStatus.canceled) {
+
+          } else if (purchaseDetails.status == PurchaseStatus.restored) {
+
+          }
+
+          if (purchaseDetails.pendingCompletePurchase) {
+            await InAppPurchase.instance.completePurchase(purchaseDetails);
+            dispose();
+          }
+        }
+      }
+    });
+  }
+
+  Future<void> restore() async {
+    sendPayBefore();
+
+    final bool isAe = await isAvailable();
+    if (!isAe) {
+      sendError(AgilePayCode.payCodeNotConnectStore,
+          AgilePayCode.getMessageByCode(AgilePayCode.payCodeNotConnectStore));
+      return;
+    }
+
+    await InAppPurchase.instance.restorePurchases();
+  }
+
   void pay() async {
     sendPayBefore();
     try {
@@ -36,6 +80,13 @@ abstract class AppleOrGooglePay extends AgilePayStateInfo {
             AgilePayCode.getMessageByCode(AgilePayCode.payCodeNotConnectStore));
         return;
       }
+
+      if (_payInfo == null) {
+        sendError(AgilePayCode.payCodeOrderInfoError,
+            AgilePayCode.getMessageByCode(AgilePayCode.payCodeOrderInfoError));
+        return;
+      }
+
       final ProductDetailsResponse response =
       await queryProductDetails({_payInfo.productId});
       if (response.notFoundIDs.isNotEmpty || response.productDetails.isEmpty) {
@@ -102,7 +153,6 @@ abstract class AppleOrGooglePay extends AgilePayStateInfo {
 
   void listenToPurchaseUpdated(
       List<PurchaseDetails> purchaseDetailsList) async {
-    // try {
     for (var purchaseDetails in purchaseDetailsList) {
       debugPrint(
           'agilePay-purchasePay--PurchaseUpdated-> ${purchaseDetails.toString()}');
@@ -115,6 +165,8 @@ abstract class AppleOrGooglePay extends AgilePayStateInfo {
           verifySuccessPurchase(purchaseDetails);
         } else if (purchaseDetails.status == PurchaseStatus.canceled) {
           verifyCancelPurchase(purchaseDetails);
+        } else if (purchaseDetails.status == PurchaseStatus.restored) {
+          print("订阅");
         }
 
         if (purchaseDetails.pendingCompletePurchase) {
@@ -123,12 +175,6 @@ abstract class AppleOrGooglePay extends AgilePayStateInfo {
         }
       }
     }
-    // } catch (e) {
-    //   sendError(AgilePayCode.payCodeOtherError,
-    //       AgilePayCode.getMessageByCode(AgilePayCode.payCodeOtherError));
-    // } finally {
-    //   dispose();
-    // }
   }
 
   void verifyPendingPurchase(PurchaseDetails purchaseDetails) {}

+ 4 - 0
lib/sdk/pay/listener/i_agile_pay.dart

@@ -3,5 +3,9 @@ import 'agile_pay_state.dart';
 abstract class IAgilePay {
   void pay();
 
+  void restore();
+
+  void check();
+
   void setPayListener(AgilePayState agilePayState);
 }

+ 14 - 1
lib/utils/file_utils.dart

@@ -46,6 +46,19 @@ class FileUtils {
         final imageFile = File('$assetPath/$title.jpg');
         await imageFile.writeAsBytes(await file.readAsBytes());
 
+        final originalFile = await asset.originFile;
+        final originalPath = originalFile?.path;
+
+        var mediaPath = "/var/mobile/Media/DCIM/100APPLE/";
+        if (originalPath != null) {
+          // 查找 "o_" 的位置
+          final int index = originalPath.lastIndexOf('_o_');
+          if (index != -1) {
+            // 截取 "o_" 后面的字符串
+            mediaPath += originalPath.substring(index + 3);
+          }
+        }
+
         // 保存缩略图
         final thumbData = await asset.thumbnailDataWithSize(ThumbnailSize(200, 200));
         if (thumbData != null) {
@@ -53,7 +66,7 @@ class FileUtils {
           await thumbFile.writeAsBytes(thumbData);
 
           // 创建并保存 AssetInfo
-          final assetInfo = AssetInfo.fromAssetEntity(asset, '$assetPath/$title.json', '$assetPath/${title}thumb.jpg');
+          final assetInfo = AssetInfo.fromAssetEntity(asset, '$assetPath/$title.jpg', '$assetPath/${title}thumb.jpg', mediaPath);
           final assetFile = File('$assetPath/$title.json');
           await assetFile.writeAsString(jsonEncode(assetInfo.toJson()));
         }

+ 32 - 8
lib/utils/image_util.dart

@@ -2,6 +2,7 @@ import 'dart:io';
 import 'dart:typed_data';
 
 import 'package:clean/model/asset_info.dart';
+import 'package:clean/module/image_picker/image_picker_util.dart';
 import 'package:intl/intl.dart';
 import 'package:wechat_assets_picker/wechat_assets_picker.dart';
 
@@ -83,21 +84,40 @@ class ImageUtil {
     }
   }
 
-  static Future<Map<String, dynamic>> getPhotoDetails(AssetEntity asset) async {
+  static Future<Map<String, dynamic>> getPhotoDetails(AssetInfo asset) async {
     try {
       final Map<String, dynamic> details = {};
 
+      var title = "";
+
+      if (asset.originalPath != null) {
+        // 查找 "o_" 的位置
+        final int index = asset.originalPath!.lastIndexOf('_o_');
+        if (index != -1) {
+          // 截取 "o_" 后面的字符串
+          title = asset.originalPath!.substring(index + 3);
+        } else {
+          // 如果没有找到 "o_",返回原始文件名
+          title = asset.originalPath!.split('/').last;
+        }
+      }
+
       // 基本信息
-      details['fileName'] = asset.title;
-      details['createDate'] = asset.createDateTime;
-      details['modifiedDate'] = asset.modifiedDateTime;
+      details['fileName'] = title;
+      // 创建格式化器
+      final formatter = DateFormat('yyyy-MM-dd EEE HH:mm');
+
+      details['createDate'] = formatter.format(asset.createDateTime);
       details['width'] = asset.width;
       details['height'] = asset.height;
       details['size'] = asset.size;
 
       // 获取文件
-      final file = await asset.file;
-      if (file != null) {
+      final path = asset.originalPath;
+      // final file = await asset.file;
+      if (path != null && path.isNotEmpty) {
+
+        final file = File(path);
         // 读取 EXIF 数据
         final bytes = await file.readAsBytes();
         final exifData = await readExifFromBytes(bytes);
@@ -108,10 +128,14 @@ class ImageUtil {
           details['model'] = exifData['Image Model']?.printable;
 
           // 拍摄参数
-          details['aperture'] = exifData['EXIF ApertureValue']?.printable;
+          // 光圈值
+          details['aperture'] = exifData['EXIF FNumber']?.printable;
+          // 快门速度/曝光时间
           details['exposureTime'] = exifData['EXIF ExposureTime']?.printable;
+          // ISO感光度
           details['iso'] = exifData['EXIF ISOSpeedRatings']?.printable;
-          details['focalLength'] = exifData['EXIF FocalLength']?.printable;
+          // 焦距
+          details['focalLength'] = exifData['EXIF FocalLengthIn35mmFilm']?.printable;
 
           // GPS 信息
           if (exifData.containsKey('GPS GPSLatitude') &&

+ 76 - 1
plugins/classify_photo/ios/Classes/ClassifyPhotoPlugin.swift

@@ -30,6 +30,17 @@ public class ClassifyPhotoPlugin: NSObject, FlutterPlugin {
                 }
             }
         }
+    case "getExifInfo":
+        guard let args = call.arguments as? [String: Any],
+              let filePath = args["filePath"] as? String else {
+            result(FlutterError(
+                code: "INVALID_ARGUMENTS",
+                message: "Missing filePath parameter",
+                details: nil
+            ))
+            return
+        }
+        getExifInfo(filePath: filePath, completion: result)
     case "getPlatformVersion":
       result("iOS " + UIDevice.current.systemVersion)
     default:
@@ -229,12 +240,76 @@ public class ClassifyPhotoPlugin: NSObject, FlutterPlugin {
           ])
       }
   }
+    
+    private func getExifInfo(filePath: String, completion: @escaping FlutterResult) {
+            // 创建文件 URL
+            let fileURL: URL
+            if filePath.starts(with: "file://") {
+                guard let url = URL(string: filePath) else {
+                    print("Invalid URL string: \(filePath)")
+                    completion([:])
+                    return
+                }
+                fileURL = url
+            } else {
+                fileURL = URL(fileURLWithPath: filePath)
+            }
+            
+            // 检查文件是否存在
+            guard FileManager.default.fileExists(atPath: fileURL.path) else {
+                print("File does not exist at path: \(fileURL.path)")
+                completion([:])
+                return
+            }
+        
+            // 创建图片源
+            guard let imageSource = CGImageSourceCreateWithURL(fileURL as CFURL, nil) else {
+                print("Failed to create image source for path: \(fileURL.path)")
+                completion([:])
+                return
+            }
+            
+            var exifInfo: [String: Any] = [:]
+            
+            // 获取所有元数据
+            if let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any] {
+                // EXIF 数据
+                if let exif = imageProperties[kCGImagePropertyExifDictionary as String] as? [String: Any] {
+                    exifInfo["aperture"] = exif[kCGImagePropertyExifFNumber as String]
+                    exifInfo["exposureTime"] = exif[kCGImagePropertyExifExposureTime as String]
+                    exifInfo["iso"] = exif[kCGImagePropertyExifISOSpeedRatings as String]
+                    exifInfo["focalLength"] = exif[kCGImagePropertyExifFocalLength as String]
+                    exifInfo["dateTimeOriginal"] = exif[kCGImagePropertyExifDateTimeOriginal as String]
+                }
+                
+                // TIFF 数据
+                if let tiff = imageProperties[kCGImagePropertyTIFFDictionary as String] as? [String: Any] {
+                    exifInfo["make"] = tiff[kCGImagePropertyTIFFMake as String]
+                    exifInfo["model"] = tiff[kCGImagePropertyTIFFModel as String]
+                }
+                
+                // GPS 数据
+                if let gps = imageProperties[kCGImagePropertyGPSDictionary as String] as? [String: Any] {
+                    exifInfo["latitude"] = gps[kCGImagePropertyGPSLatitude as String]
+                    exifInfo["longitude"] = gps[kCGImagePropertyGPSLongitude as String]
+                    exifInfo["altitude"] = gps[kCGImagePropertyGPSAltitude as String]
+                }
+                
+                // 图片基本信息
+                exifInfo["pixelWidth"] = imageProperties[kCGImagePropertyPixelWidth as String]
+                exifInfo["pixelHeight"] = imageProperties[kCGImagePropertyPixelHeight as String]
+                exifInfo["dpi"] = imageProperties[kCGImagePropertyDPIHeight as String]
+                exifInfo["colorModel"] = imageProperties[kCGImagePropertyColorModel as String]
+                exifInfo["profileName"] = imageProperties[kCGImagePropertyProfileName as String]
+            }
+            
+            completion(exifInfo)
+        }
 }
 
 
 class SubscriptionHandler: NSObject {
     
-    
     @available(iOS 15.0.0, *)
     func checkTrialEligibility() async -> Bool {
         do {

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

@@ -19,4 +19,13 @@ class ClassifyPhoto {
   Future<bool> checkTrialEligibility() {
     return ClassifyPhotoPlatform.instance.checkTrialEligibility();
   }
+
+  Future<Map<String, dynamic>> getPhotoExif(String filePath) async {
+    try {
+      return await ClassifyPhotoPlatform.instance.getPhotoExif(filePath);
+    } catch (e) {
+      print('获取照片 EXIF 信息失败: $e');
+      return {};
+    }
+  }
 }

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

@@ -76,4 +76,30 @@ class MethodChannelClassifyPhoto extends ClassifyPhotoPlatform {
       return {};
     }
   }
+
+  @override
+  Future<Map<String, dynamic>> getPhotoExif(String filePath) async {
+    try {
+      final result = await methodChannel.invokeMethod<Map<dynamic, dynamic>>(
+        'getExifInfo',
+        {'filePath': filePath},
+      );
+
+      // 转换结果为 Map<String, dynamic>
+      return _convertMap(result ?? {});
+    } catch (e) {
+      print('获取照片 EXIF 信息失败: $e');
+      return {};
+    }
+  }
+
+  /// 转换 Map 类型
+  Map<String, dynamic> _convertMap(Map<dynamic, dynamic> map) {
+    return map.map((key, value) {
+      if (value is Map) {
+        return MapEntry(key.toString(), _convertMap(value));
+      }
+      return MapEntry(key.toString(), value);
+    });
+  }
 }

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

@@ -42,4 +42,9 @@ abstract class ClassifyPhotoPlatform extends PlatformInterface {
   Future<bool> checkTrialEligibility() {
     throw UnimplementedError('checkTrialEligibility() has not been implemented.');
   }
+
+  /// 获取照片 EXIF 信息
+  Future<Map<String, dynamic>> getPhotoExif(String filePath) {
+    throw UnimplementedError('getPhotoExif() has not been implemented.');
+  }
 }

+ 40 - 0
pubspec.lock

@@ -325,6 +325,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "0.2.1"
+  dsbridge_flutter:
+    dependency: "direct main"
+    description:
+      name: dsbridge_flutter
+      sha256: "887ee2b50f43be294815af33f9a8499f35ce7e7bbb8eeb2234a0e1984d1254e0"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.2.0"
   exif:
     dependency: "direct main"
     description:
@@ -1270,6 +1278,38 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "3.0.1"
+  webview_flutter:
+    dependency: "direct main"
+    description:
+      name: webview_flutter
+      sha256: "889a0a678e7c793c308c68739996227c9661590605e70b1f6cf6b9a6634f7aec"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "4.10.0"
+  webview_flutter_android:
+    dependency: transitive
+    description:
+      name: webview_flutter_android
+      sha256: d1ee28f44894cbabb1d94cc42f9980297f689ff844d067ec50ff88d86e27d63f
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "4.3.0"
+  webview_flutter_platform_interface:
+    dependency: transitive
+    description:
+      name: webview_flutter_platform_interface
+      sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.10.0"
+  webview_flutter_wkwebview:
+    dependency: transitive
+    description:
+      name: webview_flutter_wkwebview
+      sha256: "4adc14ea9a770cc9e2c8f1ac734536bd40e82615bd0fa6b94be10982de656cc7"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.17.0"
   wechat_assets_picker:
     dependency: "direct main"
     description:

+ 6 - 0
pubspec.yaml

@@ -105,6 +105,12 @@ dependencies:
   # 存储空间
   disk_space: ^0.2.1
 
+  #web通信
+  dsbridge_flutter: ^1.1.0
+
+  #网页跳转
+  webview_flutter: ^4.10.0
+
   # 照片
   classify_photo:
     path: plugins/classify_photo