فهرست منبع

[feat]完成隐私空间存储照片和界面

Destiny 1 سال پیش
والد
کامیت
0a9e5bc8b5

BIN
assets/images/icon_common_back.webp


BIN
assets/images/icon_privacy_delete.webp


BIN
assets/images/icon_privacy_empty_image.webp


BIN
assets/images/icon_privacy_lock.webp


BIN
assets/images/icon_privacy_unlock.webp


+ 62 - 0
ios/Podfile

@@ -40,5 +40,67 @@ end
 post_install do |installer|
   installer.pods_project.targets.each do |target|
     flutter_additional_ios_build_settings(target)
+
+target.build_configurations.each do |config|
+      # You can remove unused permissions here
+      # for more information: https://github.com/BaseflowIT/flutter-permission-handler/blob/master/permission_handler/ios/Classes/PermissionHandlerEnums.h
+      # e.g. when you don't need camera permission, just add 'PERMISSION_CAMERA=0'
+      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
+        '$(inherited)',
+
+        ## dart: PermissionGroup.calendar
+        'PERMISSION_EVENTS=1',
+        
+        ## dart: PermissionGroup.calendarFullAccess
+        'PERMISSION_EVENTS_FULL_ACCESS=1',
+
+        ## dart: PermissionGroup.reminders
+        'PERMISSION_REMINDERS=1',
+
+        ## dart: PermissionGroup.contacts
+        'PERMISSION_CONTACTS=1',
+
+        ## dart: PermissionGroup.camera
+        'PERMISSION_CAMERA=1',
+
+        ## dart: PermissionGroup.microphone
+        'PERMISSION_MICROPHONE=1',
+
+        ## dart: PermissionGroup.speech
+        'PERMISSION_SPEECH_RECOGNIZER=1',
+
+        ## dart: PermissionGroup.photos
+        'PERMISSION_PHOTOS=1',
+
+        ## The 'PERMISSION_LOCATION' macro enables the `locationWhenInUse` and `locationAlways` permission. If
+        ## the application only requires `locationWhenInUse`, only specify the `PERMISSION_LOCATION_WHENINUSE`
+        ## macro.
+        ##
+        ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
+        'PERMISSION_LOCATION=1',
+        'PERMISSION_LOCATION_WHENINUSE=0',
+
+        ## dart: PermissionGroup.notification
+        'PERMISSION_NOTIFICATIONS=1',
+
+        ## dart: PermissionGroup.mediaLibrary
+        'PERMISSION_MEDIA_LIBRARY=1',
+
+        ## dart: PermissionGroup.sensors
+        'PERMISSION_SENSORS=1',
+
+        ## dart: PermissionGroup.bluetooth
+        'PERMISSION_BLUETOOTH=1',
+
+        ## dart: PermissionGroup.appTrackingTransparency
+        'PERMISSION_APP_TRACKING_TRANSPARENCY=1',
+
+        ## dart: PermissionGroup.criticalAlerts
+        'PERMISSION_CRITICAL_ALERTS=1',
+
+        ## dart: PermissionGroup.criticalAlerts
+        'PERMISSION_ASSISTANT=1',
+      ]
+    end
   end
 end

+ 34 - 1
ios/Podfile.lock

@@ -1,40 +1,73 @@
 PODS:
   - app_tracking_transparency (0.0.1):
     - Flutter
+  - camera_avfoundation (0.0.1):
+    - Flutter
   - device_info_plus (0.0.1):
     - Flutter
   - Flutter (1.0.0)
   - package_info_plus (0.4.5):
     - Flutter
+  - path_provider_foundation (0.0.1):
+    - Flutter
+    - FlutterMacOS
   - permission_handler_apple (9.3.0):
     - Flutter
+  - photo_manager (2.0.0):
+    - Flutter
+    - FlutterMacOS
+  - sensors_plus (0.0.1):
+    - Flutter
+  - video_player_avfoundation (0.0.1):
+    - Flutter
+    - FlutterMacOS
 
 DEPENDENCIES:
   - app_tracking_transparency (from `.symlinks/plugins/app_tracking_transparency/ios`)
+  - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`)
   - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
   - Flutter (from `Flutter`)
   - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
+  - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
   - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
+  - 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`)
 
 EXTERNAL SOURCES:
   app_tracking_transparency:
     :path: ".symlinks/plugins/app_tracking_transparency/ios"
+  camera_avfoundation:
+    :path: ".symlinks/plugins/camera_avfoundation/ios"
   device_info_plus:
     :path: ".symlinks/plugins/device_info_plus/ios"
   Flutter:
     :path: Flutter
   package_info_plus:
     :path: ".symlinks/plugins/package_info_plus/ios"
+  path_provider_foundation:
+    :path: ".symlinks/plugins/path_provider_foundation/darwin"
   permission_handler_apple:
     :path: ".symlinks/plugins/permission_handler_apple/ios"
+  photo_manager:
+    :path: ".symlinks/plugins/photo_manager/ios"
+  sensors_plus:
+    :path: ".symlinks/plugins/sensors_plus/ios"
+  video_player_avfoundation:
+    :path: ".symlinks/plugins/video_player_avfoundation/darwin"
 
 SPEC CHECKSUMS:
   app_tracking_transparency: e169b653478da7bb15a6c61209015378ca73e375
+  camera_avfoundation: dd002b0330f4981e1bbcb46ae9b62829237459a4
   device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
   package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
+  path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
   permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
+  photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
+  sensors_plus: 7229095999f30740798f0eeef5cd120357a8f4f2
+  video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
 
-PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796
+PODFILE CHECKSUM: ad7614c9873991faf6165fbd46dcff66209d9783
 
 COCOAPODS: 1.16.2

+ 4 - 0
ios/Runner/Info.plist

@@ -2,6 +2,10 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
+	<key>NSCameraUsageDescription</key>
+	<string>本App需要使用拍照权限用于获取照片</string>
+	<key>NSPhotoLibraryUsageDescription</key>
+	<string>本App需要使用相册权限用于获取照片</string>
 	<key>CFBundleDevelopmentRegion</key>
 	<string>$(DEVELOPMENT_LANGUAGE)</string>
 	<key>CFBundleDisplayName</key>

+ 118 - 0
lib/model/asset_info.dart

@@ -0,0 +1,118 @@
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:wechat_assets_picker/wechat_assets_picker.dart';
+
+class AssetInfo {
+  final String id;
+  final int width;
+  final int height;
+  final int duration;
+  final int orientation;
+  final int typeInt;
+  final int? createDateSecond;
+  final DateTime createDateTime;
+  final DateTime modifiedDateTime;
+  final String? title;
+  final String? relativePath;
+  final String? localPath;
+  final String? filePath;
+  final String? thumbFilePath;
+
+  AssetInfo({
+    required this.id,
+    required this.width,
+    required this.height,
+    required this.duration,
+    required this.orientation,
+    required this.typeInt,
+    this.createDateSecond,
+    required this.createDateTime,
+    required this.modifiedDateTime,
+    this.localPath,
+    this.title,
+    this.relativePath,
+    this.filePath,
+    this.thumbFilePath,
+  });
+
+  // 从 AssetEntity 创建
+  factory AssetInfo.fromAssetEntity(
+      AssetEntity entity, String filePath, String thumbFilePath) {
+    return AssetInfo(
+      id: entity.id,
+      width: entity.width,
+      height: entity.height,
+      duration: entity.duration,
+      orientation: entity.orientation,
+      typeInt: entity.typeInt,
+      createDateSecond: entity.createDateSecond,
+      createDateTime: entity.createDateTime,
+      modifiedDateTime: entity.modifiedDateTime,
+      title: entity.title,
+      relativePath: entity.relativePath,
+      filePath: filePath,
+      thumbFilePath: thumbFilePath,
+    );
+  }
+
+  // 转换为 JSON
+  Map<String, dynamic> toJson() => {
+        'id': id,
+        'width': width,
+        'height': height,
+        'duration': duration,
+        'orientation': orientation,
+        'typeInt': typeInt,
+        'createDateSecond': createDateSecond,
+        'createDateTime': createDateTime.toIso8601String(),
+        'modifiedDateTime': modifiedDateTime.toIso8601String(),
+        'title': title,
+        'relativePath': relativePath,
+        'localPath': localPath,
+        'file': filePath,
+        'thumbFile': thumbFilePath,
+      };
+
+  // 从 JSON 创建
+  factory AssetInfo.fromJson(Map<String, dynamic> json) => AssetInfo(
+        id: json['id'] as String,
+        width: json['width'] as int,
+        height: json['height'] as int,
+        duration: json['duration'] as int,
+        orientation: json['orientation'] as int,
+        // isAll: json['isAll'] as bool,
+        typeInt: json['typeInt'] as int,
+        createDateSecond: json['createDateSecond'] as int?,
+        createDateTime: DateTime.parse(json['createDateTime'] as String),
+        modifiedDateTime: DateTime.parse(json['modifiedDateTime'] as String),
+        title: json['title'] as String?,
+        relativePath: json['relativePath'] as String?,
+        localPath: json['localPath'] as String?,
+        filePath: json['filePath'] as String?,
+        thumbFilePath: json['thumbFilePath'] as String?,
+      );
+
+  // MARK: - To Do
+  // 转换为 AssetEntity
+  Future<AssetEntity?> toAssetEntity() async {
+    return await AssetEntity.fromId(id);
+  }
+
+  // 获取文件对象
+  File get file => File(filePath!);
+
+  // 获取缩略图数据
+  Future<Uint8List?> get thumbnailData async {
+    File thumbFile = File(thumbFilePath!);
+    try {
+      if (await thumbFile.exists()) {
+        return await thumbFile.readAsBytes();
+      }
+      return null;
+    } catch (e) {
+      print('获取缩略图失败: $e');
+      return null;
+    }
+  }
+}

+ 1 - 0
lib/model/local_image.dart

@@ -0,0 +1 @@
+ 

+ 1 - 1
lib/module/home/home_view.dart

@@ -52,7 +52,7 @@ class HomePage extends BaseView<HomeController> {
 
   Widget titleCard() {
     return Padding(
-      padding: EdgeInsets.only(top: 29.h, left: 16.w, right: 16.w),
+      padding: EdgeInsets.only(top: 0.h, left: 16.w, right: 16.w),
       child: Row(
         mainAxisAlignment: MainAxisAlignment.spaceBetween,
         children: [

+ 20 - 0
lib/module/image_picker/image_picker_assets.dart

@@ -0,0 +1,20 @@
+import 'dart:typed_data';
+import 'package:get/get.dart';
+import 'package:wechat_assets_picker/wechat_assets_picker.dart';
+
+class ImagePickAssets {
+  static Future<List<AssetEntity>?> pick() async {
+    return AssetPicker.pickAssets(
+      Get.context!,
+      pickerConfig: AssetPickerConfig(
+        textDelegate: CustomAssetPickerTextDelegate(),
+        requestType: RequestType.image,
+      ),
+    );
+  }
+}
+
+class CustomAssetPickerTextDelegate extends AssetPickerTextDelegate {
+  @override
+  String get confirm => 'upload';
+}

+ 2 - 0
lib/module/image_picker/image_picker_util.dart

@@ -0,0 +1,2 @@
+import 'dart:typed_data' as typed;
+// ... 其他导入 ... 

+ 32 - 0
lib/module/image_picker/image_picler_util.dart

@@ -0,0 +1,32 @@
+import 'dart:io';
+
+import 'package:photo_manager/photo_manager.dart';
+
+class ImagePickerUtil {
+  ImagePickerUtil._();
+
+  static const RequestType permissionType = RequestType.image;
+
+  //申请权限
+  static Future<bool> requestPermissionExtend() async {
+    final PermissionState ps = await PhotoManager.requestPermissionExtend(
+        requestOption: const PermissionRequestOption(
+            androidPermission: AndroidPermission(
+              type: permissionType,
+              mediaLocation: false,
+            )));
+    return ps.hasAccess;
+  }
+
+  //判断是否有权限
+  static Future<bool> hasPermission() async {
+    final PermissionState ps = await PhotoManager.getPermissionState(
+        requestOption: const PermissionRequestOption(
+            androidPermission: AndroidPermission(
+              type: permissionType,
+              mediaLocation: false,
+            )));
+    return ps.hasAccess;
+  }
+
+}

+ 7 - 24
lib/module/main/main_controller.dart

@@ -1,43 +1,26 @@
+import 'dart:io';
 import 'dart:ui';
+import 'dart:typed_data' as typed;
 
 import 'package:get/get.dart';
+import 'package:wechat_assets_picker/wechat_assets_picker.dart';
 
 import '../../base/base_controller.dart';
 import '../../resource/assets.gen.dart';
+import '../../utils/file_utils.dart';
 
 class MainController extends BaseController {
 
+  final _currentIndex = 0.obs;
+  int get currentIndex => _currentIndex.value;
+
   @override
   void onInit() {
     super.onInit();
   }
 
-  final List<TabBean> tabBeans = [
-    TabBean(
-        Assets.images.iconTabHomeUnselect.path,
-        Assets.images.iconTabHomeSelected.path),
-    TabBean(
-        Assets.images.iconTabMoreUnselect.path,
-        Assets.images.iconTabMoreSelected.path),
-  ];
-
-  final _currentIndex = 0.obs;
-
-  int get currentIndex => _currentIndex.value;
-
   void changeIndex(int index) {
     _currentIndex.value = index;
   }
 
-  void updateIndex(int index) {
-    _currentIndex.value = index;
-  }
-}
-
-
-class TabBean {
-  final String normalIcon;
-  final String selectedIcon;
-
-  TabBean(this.normalIcon, this.selectedIcon);
 }

+ 70 - 60
lib/module/more/more_view.dart

@@ -1,8 +1,11 @@
 import 'package:clean/base/base_view.dart';
 import 'package:clean/module/more/more_controller.dart';
+import 'package:clean/router/app_pages.dart';
 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:get/get_core/src/get_main.dart';
 
 import '../../resource/assets.gen.dart';
 
@@ -21,7 +24,6 @@ class MorePage extends BaseView<MoreController> {
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
             Container(
-              margin: EdgeInsets.only(left: 2.w),
               child: Text(
                 "CleanPro",
                 style: TextStyle(
@@ -58,7 +60,7 @@ class MorePage extends BaseView<MoreController> {
                     Assets.images.iconMorePrivacy
                         .image(height: 72.w, width: 72.w),
                     onTap: () {
-
+                      Get.toNamed(RoutePath.privacy);
                     }
                   ),
                   SizedBox(height: 14.h),
@@ -102,75 +104,83 @@ class MorePage extends BaseView<MoreController> {
   }
 
   Widget _buildStoreCard() {
-    return Stack(
-      children: [
-        Assets.images.iconMoreStoreCard.image(),
-        Positioned(
-          left: 12.w,
-          bottom: 15.h,
-          child: Container(
-            width: 107.w,
-            height: 30.h,
-            decoration: BoxDecoration(
-              border: Border.all(
-                color: '#FFFFFF'.color.withOpacity(0.18),
-                width: 1,
-              ),
-              borderRadius: BorderRadius.all(
-                Radius.circular(15.h),
+    return GestureDetector(
+      onTap: () {
+
+      },
+      child: Stack(
+        children: [
+          Assets.images.iconMoreStoreCard.image(),
+          Positioned(
+            left: 12.w,
+            bottom: 15.h,
+            child: Container(
+              width: 107.w,
+              height: 30.h,
+              decoration: BoxDecoration(
+                border: Border.all(
+                  color: '#FFFFFF'.color.withOpacity(0.18),
+                  width: 1,
+                ),
+                borderRadius: BorderRadius.all(
+                  Radius.circular(15.h),
+                ),
               ),
-            ),
-            child: Row(
-              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
-              children: [
-                Text(
-                  "Get more",
-                  style: TextStyle(
+              child: Row(
+                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+                children: [
+                  Text(
+                    "Get more",
+                    style: TextStyle(
+                      color: Colors.white,
+                      fontWeight: FontWeight.w500,
+                      fontSize: 14.sp,
+                    ),
+                  ),
+                  Icon(
+                    Icons.arrow_forward_ios,
+                    size: 10.w,
                     color: Colors.white,
-                    fontWeight: FontWeight.w500,
-                    fontSize: 14.sp,
                   ),
-                ),
-                Icon(
-                  Icons.arrow_forward_ios,
-                  size: 10.w,
-                  color: Colors.white,
-                ),
-              ],
+                ],
+              ),
             ),
           ),
-        ),
-      ],
+        ],
+      ),
     );
   }
 
   Widget _buildCustomCard(String title, Image bg, Image icon, {required Function() onTap}) {
-    return Stack(
-      children: [
-        bg,
-        Positioned(
-          top: 12.h,
-          left: 22.w,
-          child: Text(
-            title,
-            style: TextStyle(
-              color: Colors.white,
-              fontWeight: FontWeight.w500,
-              fontSize: 20.sp,
+    return GestureDetector(
+      onTap: onTap,
+      child: Stack(
+        children: [
+          bg,
+          Positioned(
+            top: 12.h,
+            left: 22.w,
+            child: Text(
+              title,
+              style: TextStyle(
+                color: Colors.white,
+                fontWeight: FontWeight.w500,
+                fontSize: 20.sp,
+              ),
             ),
           ),
-        ),
-        Positioned(
-          bottom: 12.h,
-          left: 22.w,
-          child: Assets.images.iconMoreBack.image(height: 28.w, width: 28.w),
-        ),
-        Positioned(
-          right: 23.w,
-          top: 12.h,
-          child: icon,
-        ),
-      ],
+          Positioned(
+            bottom: 12.h,
+            left: 22.w,
+            child: Assets.images.iconMoreBack.image(height: 28.w, width: 28.w),
+          ),
+          Positioned(
+            right: 23.w,
+            top: 12.h,
+            child: icon,
+          ),
+        ],
+      ),
     );
   }
 }

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

@@ -0,0 +1,235 @@
+import 'package:clean/base/base_controller.dart';
+import 'package:clean/model/asset_info.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';
+import 'package:permission_handler/permission_handler.dart';
+import 'package:wechat_assets_picker/wechat_assets_picker.dart';
+import 'package:wechat_camera_picker/wechat_camera_picker.dart';
+
+import '../../utils/toast_util.dart';
+import '../image_picker/image_picker_assets.dart';
+
+import 'package:intl/intl.dart';
+
+import 'dart:typed_data';
+import 'dart:io';
+
+class PrivacyController extends BaseController {
+  late var passwordStr = "".obs;
+
+  late var isUnlock = false.obs;
+
+  late List<AssetEntity>? imageList;
+
+  // 存储所有图片,按月份分组
+  final assetsByMonth = <String, List<AssetInfo>>{}.obs;
+
+  // 获取月份数量
+  int get monthCount => assetsByMonth.length;
+
+  // 获取总图片数量
+  int get totalAssetCount =>
+      assetsByMonth.values.fold(0, (sum, list) => sum + list.length);
+
+  @override
+  void onInit() {
+    // TODO: implement onInit
+    super.onInit();
+
+    loadAssets();
+  }
+
+  // 加载并分组图片
+  Future<void> loadAssets() async {
+    final imageList = await FileUtils.getAllAssets();
+    if (imageList.isEmpty) return;
+
+    // 清空现有数据
+    assetsByMonth.clear();
+
+    // 按月份分组
+    for (var asset in imageList) {
+      final monthKey = ImageUtil.getMonthKey(asset.createDateTime);
+      if (!assetsByMonth.containsKey(monthKey)) {
+        assetsByMonth[monthKey] = [];
+      }
+      assetsByMonth[monthKey]!.add(asset);
+    }
+
+    // 对每个月份内的图片按时间排序(新的在前)
+    assetsByMonth.forEach((key, list) {
+      list.sort((a, b) => b.createDateTime.compareTo(a.createDateTime));
+    });
+
+    // 打印分组结果
+    assetsByMonth.forEach((key, assets) {
+      print('${ImageUtil.formatMonthKey(key)}: ${assets.length} photos');
+    });
+  }
+
+  // // 生成月份 key (用于内部存储)
+  // String _getMonthKey(DateTime date) {
+  //   return '${date.year}-${date.month.toString().padLeft(2, '0')}';
+  // }
+  //
+  // // 格式化月份显示 (例如: Jan 2025)
+  // String _formatMonthKey(String monthKey) {
+  //   final parts = monthKey.split('-');
+  //   if (parts.length == 2) {
+  //     final date = DateTime(int.parse(parts[0]), int.parse(parts[1]));
+  //     return DateFormat('MMM yyyy').format(date);
+  //   }
+  //   return monthKey;
+  // }
+  //
+  // // 获取指定索引的月份显示文本
+  // String getMonthText(int index) {
+  //   final monthKeys = assetsByMonth.keys.toList()
+  //     ..sort((a, b) => b.compareTo(a)); // 最新的月份在前
+  //
+  //   if (index < monthKeys.length) {
+  //     return _formatMonthKey(monthKeys[index]);
+  //   }
+  //   return '';
+  // }
+  //
+  // // 获取指定月份的图片
+  // List<AssetEntity> getMonthAssets(int index) {
+  //   final monthKeys = assetsByMonth.keys.toList()
+  //     ..sort((a, b) => b.compareTo(a)); // 最新的月份在前
+  //
+  //   if (index < monthKeys.length) {
+  //     return assetsByMonth[monthKeys[index]] ?? [];
+  //   }
+  //   return [];
+  // }
+  //
+  // // 获取缩略图数据
+  // Future<Uint8List?> getImageThumbnail(AssetEntity asset) async {
+  //   try {
+  //     return await asset.thumbnailDataWithSize(ThumbnailSize(200, 200));
+  //   } catch (e) {
+  //     print('获取缩略图失败: $e');
+  //     return null;
+  //   }
+  // }
+  //
+  // // 获取原始图片文件
+  // Future<File?> getImageFile(AssetEntity asset) async {
+  //   try {
+  //     return await asset.file;
+  //   } catch (e) {
+  //     print('获取图片文件失败: $e');
+  //     return null;
+  //   }
+  // }
+
+  // 处理输入密码逻辑
+  void inputPassword(String num) {
+    passwordStr.value = passwordStr.value + num;
+
+    if (passwordStr.value.length == 4) {
+      if (passwordStr.value != "1234") {
+        ToastUtil.show("Input Error");
+        Future.delayed(const Duration(milliseconds: 100), () {
+          passwordStr.value = "";
+        });
+      } else {
+        isUnlock.value = true;
+      }
+    }
+  }
+
+  // 上传按钮点击
+  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(asset);
+    }
+    // 重新加载图片列表
+    loadAssets();
+  }
+
+  // 开启图库
+  Future<void> openGallery() async {
+    var status = await Permission.photos.status;
+    if (status == PermissionStatus.granted) {
+      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]);
+    }
+  }
+}

+ 557 - 0
lib/module/privacy/privacy_view.dart

@@ -0,0 +1,557 @@
+import 'dart:io';
+
+import 'package:clean/model/asset_info.dart';
+import 'package:clean/module/privacy/privacy_controller.dart';
+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 '../../resource/assets.gen.dart';
+import '../more/more_controller.dart';
+import 'dart:typed_data';
+
+class PrivacyPage extends BaseView<PrivacyController> {
+  const PrivacyPage({super.key});
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Obx(() {
+      return Stack(
+        children: [
+          !controller.isUnlock.value
+              ? _buildPasswordPage()
+              : _buildPrivacySpace(context),
+          IgnorePointer(
+            child: Assets.images.bgHome.image(
+              width: 360.w,
+            ),
+          ),
+        ],
+      );
+    });
+  }
+
+  // 输入密码界面
+  Widget _buildPasswordPage() {
+    return 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: 28.h,
+            ),
+            Align(
+              child: Column(
+                children: [
+                  Assets.images.iconPrivacyLock
+                      .image(width: 70.w, height: 70.w),
+                  Text(
+                    "Input password",
+                    style: TextStyle(
+                      color: Colors.white,
+                      fontSize: 16.sp,
+                      fontWeight: FontWeight.w700,
+                    ),
+                  ),
+                  SizedBox(
+                    height: 28.h,
+                  ),
+                  Obx(() {
+                    return Row(
+                      mainAxisAlignment: MainAxisAlignment.center,
+                      children: [
+                        Container(
+                          width: 32.h,
+                          height: 32.h,
+                          decoration: BoxDecoration(
+                              border: Border.all(
+                                color: "#0279FB".color,
+                                width: 2.w,
+                              ),
+                              borderRadius:
+                                  BorderRadius.all(Radius.circular(16.h)),
+                              color: controller.passwordStr.value.isNotEmpty
+                                  ? "#0279FB".color
+                                  : Colors.transparent),
+                        ),
+                        Container(
+                          margin: EdgeInsets.only(left: 16.w),
+                          width: 32.h,
+                          height: 32.h,
+                          decoration: BoxDecoration(
+                              border: Border.all(
+                                color: "#0279FB".color,
+                                width: 2.w,
+                              ),
+                              borderRadius:
+                                  BorderRadius.all(Radius.circular(16.h)),
+                              color: controller.passwordStr.value.length >= 2
+                                  ? "#0279FB".color
+                                  : Colors.transparent),
+                        ),
+                        Container(
+                          margin: EdgeInsets.only(left: 16.w),
+                          width: 32.h,
+                          height: 32.h,
+                          decoration: BoxDecoration(
+                              border: Border.all(
+                                color: "#0279FB".color,
+                                width: 2.w,
+                              ),
+                              borderRadius:
+                                  BorderRadius.all(Radius.circular(16.h)),
+                              color: controller.passwordStr.value.length >= 3
+                                  ? "#0279FB".color
+                                  : Colors.transparent),
+                        ),
+                        Container(
+                          margin: EdgeInsets.only(left: 16.w),
+                          width: 32.h,
+                          height: 32.h,
+                          decoration: BoxDecoration(
+                              border: Border.all(
+                                color: "#0279FB".color,
+                                width: 2.w,
+                              ),
+                              borderRadius:
+                                  BorderRadius.all(Radius.circular(16.h)),
+                              color: controller.passwordStr.value.length >= 4
+                                  ? "#0279FB".color
+                                  : Colors.transparent),
+                        ),
+                      ],
+                    );
+                  }),
+                  SizedBox(
+                    height: 67.h,
+                  ),
+                  _buildPasswordInput(),
+                ],
+              ),
+            )
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildPasswordInput() {
+    return Column(
+      children: [
+        Row(
+          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+          children: [
+            _buildNumberBtn("1"),
+            _buildNumberBtn("2"),
+            _buildNumberBtn("3"),
+          ],
+        ),
+        SizedBox(height: 20.h),
+        Row(
+          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+          children: [
+            _buildNumberBtn("4"),
+            _buildNumberBtn("5"),
+            _buildNumberBtn("6"),
+          ],
+        ),
+        SizedBox(height: 20.h),
+        Row(
+          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+          children: [
+            _buildNumberBtn("7"),
+            _buildNumberBtn("8"),
+            _buildNumberBtn("9"),
+          ],
+        ),
+        SizedBox(height: 20.h),
+        Row(
+          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+          children: [
+            _buildNumberBtn(""),
+            _buildNumberBtn("0"),
+            _buildDeleteBtn(),
+          ],
+        ),
+      ],
+    );
+  }
+
+  Widget _buildNumberBtn(String num) {
+    return Opacity(
+      opacity: num.isEmpty ? 0 : 1,
+      child: GestureDetector(
+        onTap: () {
+          controller.inputPassword(num);
+        },
+        child: Container(
+          width: 76.w,
+          height: 76.w,
+          decoration: BoxDecoration(
+            color: "#FFFFFF".color.withOpacity(0.12),
+            borderRadius: BorderRadius.all(Radius.circular(38.w)),
+          ),
+          child: Align(
+            child: Text(
+              num,
+              style: TextStyle(
+                color: Colors.white,
+                fontSize: 20.sp,
+                fontWeight: FontWeight.w700,
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  Widget _buildDeleteBtn() {
+    return GestureDetector(
+      onTap: () {
+        if (controller.passwordStr.isNotEmpty) {
+          controller.passwordStr.value = controller.passwordStr.value
+              .substring(0, controller.passwordStr.value.length - 1);
+        }
+      },
+      child: Container(
+        width: 76.w,
+        height: 76.w,
+        decoration: BoxDecoration(
+          color: "#FFFFFF".color.withOpacity(0.12),
+          borderRadius: BorderRadius.all(Radius.circular(38.w)),
+        ),
+        child: Center(
+          child:
+              Assets.images.iconPrivacyDelete.image(width: 34.w, height: 24.h),
+        ),
+      ),
+    );
+  }
+
+  Widget _buildPrivacySpace(BuildContext context) {
+    return SafeArea(
+      child: Container(
+        padding: EdgeInsets.only(left: 16.w, top: 14.h, right: 16.w),
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Row(
+              mainAxisAlignment: MainAxisAlignment.spaceBetween,
+              children: [
+                GestureDetector(
+                  onTap: () {
+                    Get.back();
+                  },
+                  child: Assets.images.iconCommonBack
+                      .image(width: 28.w, height: 28.w),
+                ),
+                GestureDetector(
+                  onTap: () {
+                    Get.back();
+                  },
+                  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,
+                        ),
+                      ),
+                    ),
+                  ),
+                ),
+              ],
+            ),
+            SizedBox(
+              height: 12.h,
+            ),
+            Row(
+              mainAxisAlignment: MainAxisAlignment.spaceBetween,
+              children: [
+                Text(
+                  "Privacy Space",
+                  style: TextStyle(
+                    color: Colors.white,
+                    fontWeight: FontWeight.w700,
+                    fontSize: 24.sp,
+                  ),
+                ),
+                GestureDetector(
+                  child: Assets.images.iconPrivacyUnlock
+                      .image(width: 34.w, height: 34.w),
+                ),
+              ],
+            ),
+            // _buildEmptyPhotoView(context),
+            _buildPhotoView(),
+          ],
+        ),
+      ),
+    );
+  }
+
+  _buildEmptyPhotoView(BuildContext context) {
+    return Center(
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        crossAxisAlignment: CrossAxisAlignment.center,
+        children: [
+          SizedBox(
+            height: 130.h,
+          ),
+          // Image.memory(),
+          // Image.file(controller.file!),
+          Assets.images.iconPrivacyEmptyImage.image(width: 70.w, height: 70.w),
+          SizedBox(
+            height: 22.h,
+          ),
+          Text(
+            "Upload Files to Privacy Space",
+            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,
+                    ),
+                  ],
+                );
+              },
+            ),
+          ),
+          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,
+                    fontWeight: FontWeight.w500,
+                    fontSize: 16.sp,
+                  ),
+                ),
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  // 构建图片项
+  Widget _buildAssetItem(AssetInfo asset) {
+    return GestureDetector(
+      onTap: () => _showImageDetail(asset),
+      child: Stack(
+        children: [
+          ClipRRect(
+            borderRadius: BorderRadius.circular(8.r),
+            child: FutureBuilder<Uint8List?>(
+              future: ImageUtil.getImageThumbnail(asset),
+              builder: (context, snapshot) {
+                if (snapshot.connectionState == ConnectionState.waiting) {
+                  return Container(
+                    color: Colors.grey[200],
+                    child: Center(
+                      child: SizedBox(
+                        width: 20.w,
+                        height: 20.w,
+                        child: CircularProgressIndicator(
+                          strokeWidth: 2.w,
+                        ),
+                      ),
+                    ),
+                  );
+                }
+
+                if (snapshot.hasData && snapshot.data != null) {
+                  return Image.memory(
+                    snapshot.data!,
+                    width: double.infinity,
+                    height: double.infinity,
+                    fit: BoxFit.cover,
+                  );
+                }
+
+                return Container(
+                  color: Colors.grey[200],
+                  child: Icon(
+                    Icons.error_outline,
+                    color: Colors.grey[400],
+                  ),
+                );
+              },
+            ),
+          ),
+          // // 删除按钮
+          // Positioned(
+          //   right: 4.w,
+          //   top: 4.w,
+          //   child: GestureDetector(
+          //     // onTap: () => _deleteAsset(asset),
+          //     child: Container(
+          //       padding: EdgeInsets.all(4.w),
+          //       decoration: BoxDecoration(
+          //         color: Colors.black.withOpacity(0.5),
+          //         shape: BoxShape.circle,
+          //       ),
+          //       child: Icon(
+          //         Icons.close,
+          //         size: 16.w,
+          //         color: Colors.white,
+          //       ),
+          //     ),
+          //   ),
+          // ),
+        ],
+      ),
+    );
+  }
+
+  // 显示图片详情
+  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/router/app_pages.dart

@@ -2,6 +2,7 @@ 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';
 import 'package:clean/module/main/main_view.dart';
+import 'package:clean/module/privacy/privacy_view.dart';
 import 'package:clean/module/people_photo/people_photo_controller.dart';
 import 'package:clean/module/people_photo/people_photo_view.dart';
 import 'package:clean/module/similar_photo/similar_photo_controller.dart';
@@ -11,6 +12,7 @@ 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/privacy/privacy_controller.dart';
 
 abstract class AppPage {
   static final pages = <GetPage>[
@@ -20,6 +22,7 @@ abstract class AppPage {
 
 abstract class RoutePath {
   static const mainTab = '/mainTab';
+  static const privacy = '/privacy';
   static const peoplePhoto = '/peoplePhoto';
   static const similarPhoto = '/similarPhoto';
   static const locationsPhoto = '/locationsPhoto';
@@ -30,6 +33,7 @@ class AppBinding extends Bindings {
   void dependencies() {
     lazyPut(() => MainController());
     lazyPut(() => HomeController());
+    lazyPut(() => PrivacyController());
     lazyPut(() => PeoplePhotoController());
     lazyPut(() => SimilarPhotoController());
     lazyPut(() => LocationsPhotoController());
@@ -43,6 +47,7 @@ class AppBinding extends Bindings {
 
 final generalPages = [
   GetPage(name: RoutePath.mainTab, page: () => MainTabPage()),
+  GetPage(name: RoutePath.privacy, page: () => PrivacyPage()),
   GetPage(name: RoutePath.peoplePhoto, page: () => PeoplePhotoPage()),
   GetPage(name: RoutePath.similarPhoto, page: () => SimilarPhotoPage()),
   GetPage(name: RoutePath.locationsPhoto, page: () => LocationsPhotoPage()),

+ 1 - 0
lib/utils/asset_utils.dart

@@ -0,0 +1 @@
+ 

+ 152 - 0
lib/utils/file_utils.dart

@@ -0,0 +1,152 @@
+import 'dart:io';
+import 'dart:convert';
+import 'dart:typed_data';
+import 'package:path_provider/path_provider.dart';
+import 'package:photo_manager/photo_manager.dart';
+
+import '../model/asset_info.dart';
+
+class FileUtils {
+  /// 获取 AssetEntity 保存目录
+  static Future<String> getAssetPath() async {
+    final directory = await getApplicationDocumentsDirectory();
+    final path = '${directory.path}/assets';
+    final dir = Directory(path);
+    if (!dir.existsSync()) {
+      dir.createSync(recursive: true);
+    }
+    return path;
+  }
+
+  /// 保存 AssetEntity 到本地
+  static Future<AssetEntity?> saveAsset(AssetEntity asset) async {
+    try {
+      final assetPath = await getAssetPath();
+      final title = asset.createDateSecond;
+      final assetFile = File('$assetPath/$title.json');
+
+      // // 将 AssetEntity 转换为 AssetInfo 后再序列化
+      // final assetInfo = AssetInfo.fromAssetEntity(asset);
+      // await assetFile.writeAsString(jsonEncode(assetInfo.toJson()));
+
+      // 保存原始图片文件
+      final file = await asset.file;
+      if (file != null) {
+        final imageFile = File('$assetPath/$title.jpg');
+        await imageFile.writeAsBytes(await file.readAsBytes());
+
+        // 保存缩略图
+        final thumbData = await asset.thumbnailDataWithSize(ThumbnailSize(200, 200));
+        if (thumbData != null) {
+          final thumbFile = File('$assetPath/${title}thumb.jpg');
+          await thumbFile.writeAsBytes(thumbData);
+
+          // 创建并保存 AssetInfo
+          final assetInfo = AssetInfo.fromAssetEntity(asset, '$assetPath/$title.json', '$assetPath/${title}thumb.jpg');
+          final assetFile = File('$assetPath/$title.json');
+          await assetFile.writeAsString(jsonEncode(assetInfo.toJson()));
+        }
+      }
+
+      return asset;
+    } catch (e) {
+      print('保存 AssetEntity 失败: $e');
+      return null;
+    }
+  }
+
+  /// 从本地读取图片数据
+  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(int assetId) async {
+    try {
+      final assetPath = await getAssetPath();
+      final thumbFile = File('$assetPath/${assetId}thumb.jpg');
+      if (await thumbFile.exists()) {
+        return await thumbFile.readAsBytes();
+      }
+      return null;
+    } catch (e) {
+      print('读取缩略图数据失败: $e');
+      return null;
+    }
+  }
+
+  /// 从本地读取 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;
+    }
+  }
+
+  /// 获取目录下所有 AssetEntity
+  static Future<List<AssetInfo>> getAllAssets() async {
+    try {
+      final assetPath = await getAssetPath();
+      final assetDir = Directory(assetPath);
+      if (!await assetDir.exists()) {
+        return [];
+      }
+
+      final List<AssetInfo> assets = [];
+      final List<FileSystemEntity> entities = await assetDir.list().toList();
+
+      for (var entity in entities) {
+        if (entity is File && entity.path.endsWith('.json')) {
+          final jsonStr = await entity.readAsString();
+          final json = jsonDecode(jsonStr);
+          final assetInfo = AssetInfo.fromJson(json);
+          assets.add(assetInfo);
+          // final asset = await assetInfo.toAssetEntity();
+          // if (asset != null) {
+          //   assets.add(asset);
+          // }
+        }
+      }
+
+      return assets;
+    } catch (e) {
+      print('获取所有 AssetEntity 失败: $e');
+      return [];
+    }
+  }
+
+  /// 删除 AssetEntity 文件
+  static Future<bool> deleteAsset(String fileName) async {
+    try {
+      final assetPath = await getAssetPath();
+      final assetFile = File('$assetPath/$fileName.json');
+      if (await assetFile.exists()) {
+        await assetFile.delete();
+      }
+      return true;
+    } catch (e) {
+      print('删除 AssetEntity 失败: $e');
+      return false;
+    }
+  }
+}

+ 83 - 0
lib/utils/image_util.dart

@@ -0,0 +1,83 @@
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:clean/model/asset_info.dart';
+import 'package:intl/intl.dart';
+import 'package:wechat_assets_picker/wechat_assets_picker.dart';
+
+import 'file_utils.dart';
+
+class ImageUtil {
+
+  // 生成月份 key (用于内部存储)
+  static String getMonthKey(DateTime date) {
+    return '${date.year}-${date.month.toString().padLeft(2, '0')}';
+  }
+
+  // 格式化月份显示 (例如: Jan 2025)
+  static String formatMonthKey(String monthKey) {
+    final parts = monthKey.split('-');
+    if (parts.length == 2) {
+      final date = DateTime(int.parse(parts[0]), int.parse(parts[1]));
+      return DateFormat('MMM yyyy').format(date);
+    }
+    return monthKey;
+  }
+
+  // 获取指定索引的月份显示文本
+  static String getMonthText(Map<String, List<AssetInfo>> assets, int index) {
+    final monthKeys = assets.keys.toList()
+      ..sort((a, b) => b.compareTo(a)); // 最新的月份在前
+
+    if (index < monthKeys.length) {
+      return formatMonthKey(monthKeys[index]);
+    }
+    return '';
+  }
+
+  // 获取指定月份的图片
+  static List<AssetInfo> getMonthAssets(Map<String, List<AssetInfo>> assets, int index) {
+    final monthKeys = assets.keys.toList()
+      ..sort((a, b) => b.compareTo(a)); // 最新的月份在前
+
+    if (index < monthKeys.length) {
+      return assets[monthKeys[index]] ?? [];
+    }
+    return [];
+  }
+
+  // 获取缩略图数据
+  static Future<Uint8List?> getImageThumbnail(AssetInfo asset) async {
+    try {
+      // 先尝试从本地读取缩略图
+      final localThumb = await FileUtils.getThumbData(asset.createDateSecond!);
+      if (localThumb != null) {
+        return localThumb;
+      }
+
+      // 如果本地没有,则从 AssetEntity 获取
+      return await asset.thumbnailData;
+    } catch (e) {
+      print('获取缩略图失败: $e');
+      return null;
+    }
+  }
+
+  // 获取原始图片文件
+  static Future<File?> getImageFile(AssetInfo asset) async {
+    try {
+      // 先尝试从本地读取
+      final assetPath = await FileUtils.getAssetPath();
+      final localFile = File('$assetPath/${asset.createDateSecond}.jpg');
+      if (await localFile.exists()) {
+        return localFile;
+      }
+
+      // 如果本地没有,则从 AssetEntity 获取
+      return await asset.file;
+    } catch (e) {
+      print('获取图片文件失败: $e');
+      return null;
+    }
+  }
+}

+ 50 - 0
lib/utils/toast_util.dart

@@ -0,0 +1,50 @@
+import 'package:clean/utils/expand.dart';
+import 'package:flutter/Material.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+
+class ToastUtil {
+  ToastUtil._();
+
+  static void show(String? msg,
+      {Duration? displayTime,
+        SmartToastType? displayType = SmartToastType.normal,
+        bool? addPostFrame}) {
+    if (msg != null) {
+      if (addPostFrame == true) {
+        WidgetsBinding.instance.addPostFrameCallback((_) {
+          SmartDialog.showToast("",
+              displayType: displayType, displayTime: displayTime, builder: (_ ) => CustomToast(msg));
+        });
+      } else {
+        SmartDialog.showToast("",
+            displayType: displayType, displayTime: displayTime, builder: (_ ) => CustomToast(msg));
+      }
+    }
+  }
+}
+
+class CustomToast extends StatelessWidget {
+  const CustomToast(this.msg, {super.key});
+
+  final String msg;
+
+  @override
+  Widget build(BuildContext context) {
+    return Align(
+      alignment: Alignment.bottomCenter,
+      child: Container(
+        margin: EdgeInsets.only(bottom: 30),
+        padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
+        decoration: BoxDecoration(
+          color: "#383A3E".color,
+          borderRadius: BorderRadius.circular(10),
+        ),
+        child: Row(mainAxisSize: MainAxisSize.min, children: [
+          Text(msg, style: TextStyle(color: Colors.white)),
+        ]),
+      ),
+    );
+  }
+
+}
+

+ 310 - 1
pubspec.lock

@@ -126,6 +126,46 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "8.9.3"
+  camera:
+    dependency: transitive
+    description:
+      name: camera
+      sha256: dfa8fc5a1adaeb95e7a54d86a5bd56f4bb0e035515354c8ac6d262e35cec2ec8
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.10.6"
+  camera_android:
+    dependency: transitive
+    description:
+      name: camera_android
+      sha256: "19b7226387218864cb2388e1ad5db7db50d065222f5511254b03fc397dd21a5e"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.10.9+17"
+  camera_avfoundation:
+    dependency: transitive
+    description:
+      name: camera_avfoundation
+      sha256: c3038e6e72e284b14ad246a419f26908c08f8886d114cb8a2e351988439bfa68
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.9.17+6"
+  camera_platform_interface:
+    dependency: transitive
+    description:
+      name: camera_platform_interface
+      sha256: "953e7baed3a7c8fae92f7200afeb2be503ff1a17c3b4e4ed7b76f008c2810a31"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.9.0"
+  camera_web:
+    dependency: transitive
+    description:
+      name: camera_web
+      sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.3.5"
   characters:
     dependency: transitive
     description:
@@ -190,6 +230,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "3.2.0"
+  cross_file:
+    dependency: transitive
+    description:
+      name: cross_file
+      sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.3.4+2"
   crypto:
     dependency: transitive
     description:
@@ -198,6 +246,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "3.0.6"
+  csslib:
+    dependency: transitive
+    description:
+      name: csslib
+      sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.2"
   cupertino_icons:
     dependency: "direct main"
     description:
@@ -254,6 +310,22 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.0"
+  extended_image:
+    dependency: transitive
+    description:
+      name: extended_image
+      sha256: "85199f9233e03abc2ce2e68cbb2991648666af4a527ae4e6250935be8edfddae"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "9.1.0"
+  extended_image_library:
+    dependency: transitive
+    description:
+      name: extended_image_library
+      sha256: "9a94ec9314aa206cfa35f16145c3cd6e2c924badcc670eaaca8a3a8063a68cd7"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "4.0.5"
   fake_async:
     dependency: transitive
     description:
@@ -315,6 +387,19 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "5.0.0"
+  flutter_localizations:
+    dependency: "direct main"
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  flutter_plugin_android_lifecycle:
+    dependency: transitive
+    description:
+      name: flutter_plugin_android_lifecycle
+      sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.24"
   flutter_screenutil:
     dependency: "direct main"
     description:
@@ -381,6 +466,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.0"
+  html:
+    dependency: transitive
+    description:
+      name: html
+      sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.15.5"
   http:
     dependency: transitive
     description:
@@ -389,6 +482,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.2.2"
+  http_client_helper:
+    dependency: transitive
+    description:
+      name: http_client_helper
+      sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.0.0"
   http_multi_server:
     dependency: transitive
     description:
@@ -413,6 +514,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.2.0"
+  intl:
+    dependency: "direct main"
+    description:
+      name: intl
+      sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.19.0"
   io:
     dependency: transitive
     description:
@@ -525,6 +634,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.0"
+  nested:
+    dependency: transitive
+    description:
+      name: nested
+      sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.0"
   package_config:
     dependency: transitive
     description:
@@ -565,6 +682,54 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.1.0"
+  path_provider:
+    dependency: transitive
+    description:
+      name: path_provider
+      sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.5"
+  path_provider_android:
+    dependency: transitive
+    description:
+      name: path_provider_android
+      sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.2.15"
+  path_provider_foundation:
+    dependency: transitive
+    description:
+      name: path_provider_foundation
+      sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.4.1"
+  path_provider_linux:
+    dependency: transitive
+    description:
+      name: path_provider_linux
+      sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.2.1"
+  path_provider_platform_interface:
+    dependency: transitive
+    description:
+      name: path_provider_platform_interface
+      sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.2"
+  path_provider_windows:
+    dependency: transitive
+    description:
+      name: path_provider_windows
+      sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.3.0"
   permission_handler:
     dependency: "direct main"
     description:
@@ -621,6 +786,30 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "6.0.2"
+  photo_manager:
+    dependency: transitive
+    description:
+      name: photo_manager
+      sha256: dc26184676b26d722d656073ca8fe29203d7631ec613aed1a9679de3aa1f52c2
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.6.3"
+  photo_manager_image_provider:
+    dependency: transitive
+    description:
+      name: photo_manager_image_provider
+      sha256: b6015b67b32f345f57cf32c126f871bced2501236c405aafaefa885f7c821e4f
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.2.0"
+  platform:
+    dependency: transitive
+    description:
+      name: platform
+      sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.1.6"
   plugin_platform_interface:
     dependency: transitive
     description:
@@ -645,6 +834,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "3.1.0"
+  provider:
+    dependency: transitive
+    description:
+      name: provider
+      sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "6.1.2"
   pub_semver:
     dependency: transitive
     description:
@@ -685,6 +882,22 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "9.1.7"
+  sensors_plus:
+    dependency: transitive
+    description:
+      name: sensors_plus
+      sha256: "905282c917c6bb731c242f928665c2ea15445aa491249dea9d98d7c79dc8fd39"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "6.1.1"
+  sensors_plus_platform_interface:
+    dependency: transitive
+    description:
+      name: sensors_plus_platform_interface
+      sha256: "58815d2f5e46c0c41c40fb39375d3f127306f7742efe3b891c0b1c87e2b5cd5d"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.1"
   shelf:
     dependency: transitive
     description:
@@ -762,6 +975,22 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.3.0"
+  syncfusion_flutter_charts:
+    dependency: "direct main"
+    description:
+      name: syncfusion_flutter_charts
+      sha256: "9745a1afb3d14f2f8973a4422767d0124217d6727eacbbf4b2ed0dd66bc58887"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "28.1.38"
+  syncfusion_flutter_core:
+    dependency: transitive
+    description:
+      name: syncfusion_flutter_core
+      sha256: "12735505d616320aebe39a6fc90b6608a09116378d66aee9636b0eddf7b75971"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "28.1.38"
   term_glyph:
     dependency: transitive
     description:
@@ -826,6 +1055,54 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.1.4"
+  video_player:
+    dependency: transitive
+    description:
+      name: video_player
+      sha256: "4a8c3492d734f7c39c2588a3206707a05ee80cef52e8c7f3b2078d430c84bc17"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.9.2"
+  video_player_android:
+    dependency: transitive
+    description:
+      name: video_player_android
+      sha256: "7018dbcb395e2bca0b9a898e73989e67c0c4a5db269528e1b036ca38bcca0d0b"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.7.17"
+  video_player_avfoundation:
+    dependency: transitive
+    description:
+      name: video_player_avfoundation
+      sha256: "33224c19775fd244be2d6e3dbd8e1826ab162877bd61123bf71890772119a2b7"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.6.5"
+  video_player_platform_interface:
+    dependency: transitive
+    description:
+      name: video_player_platform_interface
+      sha256: "229d7642ccd9f3dc4aba169609dd6b5f3f443bb4cc15b82f7785fcada5af9bbb"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "6.2.3"
+  video_player_web:
+    dependency: transitive
+    description:
+      name: video_player_web
+      sha256: "881b375a934d8ebf868c7fb1423b2bfaa393a0a265fa3f733079a86536064a10"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.3.3"
+  visibility_detector:
+    dependency: transitive
+    description:
+      name: visibility_detector
+      sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.4.0+2"
   vm_service:
     dependency: transitive
     description:
@@ -866,6 +1143,30 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "3.0.1"
+  wechat_assets_picker:
+    dependency: "direct main"
+    description:
+      name: wechat_assets_picker
+      sha256: fe1dc02e68f9aabbfc63023f868f284fca3bd81d4e132a45292065e9b1de32a2
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "9.4.2"
+  wechat_camera_picker:
+    dependency: "direct main"
+    description:
+      name: wechat_camera_picker
+      sha256: f53eb68486676f27b538cb90213532394df9efd1344b47ec242c1003ef5aa18c
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "4.3.6"
+  wechat_picker_library:
+    dependency: transitive
+    description:
+      name: wechat_picker_library
+      sha256: a42e09cb85b15fc9410f6a69671371cc60aa99c4a1f7967f6593a7f665f6f47a
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.5"
   win32:
     dependency: transitive
     description:
@@ -882,6 +1183,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.1.5"
+  xdg_directories:
+    dependency: transitive
+    description:
+      name: xdg_directories
+      sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.1.0"
   xml:
     dependency: transitive
     description:
@@ -900,4 +1209,4 @@ packages:
     version: "3.1.3"
 sdks:
   dart: ">=3.6.0 <4.0.0"
-  flutter: ">=3.19.0"
+  flutter: ">=3.27.0"

+ 9 - 0
pubspec.yaml

@@ -62,8 +62,17 @@ dependencies:
   #上、下拉刷新
   pull_to_refresh: ^2.0.0
 
+  #整合的图片选择器
+  wechat_assets_picker: ^9.4.1
+
+  #拍照
+  wechat_camera_picker: ^4.3.6
+
+  #tabbar
   convex_bottom_bar: ^3.2.0
 
+  intl: ^0.19.0
+
   # The following adds the Cupertino Icons font to your application.
   # Use with the CupertinoIcons class for iOS style icons.
   cupertino_icons: ^1.0.8