Browse Source

[feat]完成隐私空间开发

Destiny 1 năm trước cách đây
mục cha
commit
fd276af375

BIN
assets/images/icon_privacy_photo_delete.webp


+ 17 - 0
ios/Podfile.lock

@@ -10,6 +10,12 @@ PODS:
   - disk_space (0.0.1):
     - Flutter
   - Flutter (1.0.0)
+  - MMKV (1.3.13):
+    - MMKVCore (~> 1.3.13)
+  - mmkv_ios (1.0.8):
+    - Flutter
+    - MMKV (< 2.0, >= 1.3.9)
+  - MMKVCore (1.3.13)
   - package_info_plus (0.4.5):
     - Flutter
   - path_provider_foundation (0.0.1):
@@ -33,6 +39,7 @@ DEPENDENCIES:
   - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
   - disk_space (from `.symlinks/plugins/disk_space/ios`)
   - Flutter (from `Flutter`)
+  - mmkv_ios (from `.symlinks/plugins/mmkv_ios/ios`)
   - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
   - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
   - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
@@ -40,6 +47,11 @@ DEPENDENCIES:
   - sensors_plus (from `.symlinks/plugins/sensors_plus/ios`)
   - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
 
+SPEC REPOS:
+  trunk:
+    - MMKV
+    - MMKVCore
+
 EXTERNAL SOURCES:
   app_tracking_transparency:
     :path: ".symlinks/plugins/app_tracking_transparency/ios"
@@ -53,6 +65,8 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/disk_space/ios"
   Flutter:
     :path: Flutter
+  mmkv_ios:
+    :path: ".symlinks/plugins/mmkv_ios/ios"
   package_info_plus:
     :path: ".symlinks/plugins/package_info_plus/ios"
   path_provider_foundation:
@@ -73,6 +87,9 @@ SPEC CHECKSUMS:
   device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
   disk_space: e94d34bbdf77954adfb39e60bde9cc5c7233eda6
   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
+  MMKV: 5854d45476fc3757bacfa7e13cc0fbcd274ab0e4
+  mmkv_ios: 75b9f18f1baf8991985e095192a2b4e35f1e06ea
+  MMKVCore: edbad9714bb70344544148f086a4a69061ff31b6
   package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
   path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
   permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2

+ 94 - 0
lib/dialog/privacy_lock_dialog.dart

@@ -0,0 +1,94 @@
+import 'package:clean/utils/expand.dart';
+import 'package:flutter/Material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+
+import '../resource/assets.gen.dart';
+
+class PrivacyLockDialog extends StatelessWidget {
+  final bool isPrivacyOn;
+  final VoidCallback? onResetPassword;
+  final VoidCallback? onSetPublic;
+
+  const PrivacyLockDialog({
+    super.key,
+    required this.isPrivacyOn,
+    this.onResetPassword,
+    this.onSetPublic,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Dialog(
+      backgroundColor: "#23232A".color,
+      shape: RoundedRectangleBorder(
+        borderRadius: BorderRadius.circular(26.r),
+      ),
+      child: Container(
+        width: 285.w,
+        padding: EdgeInsets.all(20),
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            // 锁图标
+            isPrivacyOn
+                ? Assets.images.iconPrivacyLock.image(width: 76.w, height: 76.w)
+                : Assets.images.iconPrivacyUnlock
+                    .image(width: 76.w, height: 76.w),
+            SizedBox(height: 16),
+            // 标题文本
+            Text(
+              'Privacy Mode ${isPrivacyOn ? "On" : "Off"}',
+              style: TextStyle(
+                color: Colors.white,
+                fontSize: 20.sp,
+                fontWeight: FontWeight.w700,
+              ),
+            ),
+            SizedBox(height: 24),
+
+            // 重置密码按钮
+            GestureDetector(
+              onTap: () {
+                onResetPassword!();
+              },
+              child: Container(
+                width: 180.w,
+                height: 48.h,
+                decoration: BoxDecoration(
+                  color: "#0279FB".color,
+                  borderRadius: BorderRadius.circular(10.r),
+                ),
+                child: Center(
+                  child: Text(
+                    isPrivacyOn ? 'Reset Password' : 'Set Password',
+                    style: TextStyle(
+                      fontSize: 16.sp,
+                      fontWeight: FontWeight.w500,
+                      color: Colors.white,
+                    ),
+                  ),
+                ),
+              ),
+            ),
+            SizedBox(height: 15.h),
+            // 设为公开按钮
+            Visibility(
+              visible: isPrivacyOn,
+              child: TextButton(
+                onPressed: onSetPublic,
+                child: Text(
+                  'Set as Public',
+                  style: TextStyle(
+                    color: Colors.white.withOpacity(0.7),
+                    fontSize: 16,
+                    fontWeight: FontWeight.w500,
+                  ),
+                ),
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 5 - 1
lib/main.dart

@@ -2,6 +2,7 @@ import 'package:clean/resource/colors.gen.dart';
 import 'package:clean/resource/string.gen.dart';
 import 'package:clean/resource/string_source.dart';
 import 'package:clean/router/app_pages.dart';
+import 'package:clean/utils/mmkv_util.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_localizations/flutter_localizations.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
@@ -9,7 +10,10 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
 import 'package:get/get_navigation/src/root/get_material_app.dart';
 import 'package:pull_to_refresh/pull_to_refresh.dart';
 
-void main() {
+Future<void> main() async {
+
+  await KVUtil.init();
+
   runApp(const MyApp());
 }
 

+ 4 - 2
lib/model/asset_info.dart

@@ -7,7 +7,7 @@ class AssetInfo {
   final String id;
   final int width;
   final int height;
-  final int duration;
+  final int? duration;
   final int orientation;
   final int typeInt;
   final int? createDateSecond;
@@ -18,12 +18,13 @@ class AssetInfo {
   final String? localPath;
   final String? filePath;
   final String? thumbFilePath;
+  int? size;
 
   AssetInfo({
     required this.id,
     required this.width,
     required this.height,
-    required this.duration,
+    this.duration,
     required this.orientation,
     required this.typeInt,
     this.createDateSecond,
@@ -34,6 +35,7 @@ class AssetInfo {
     this.relativePath,
     this.filePath,
     this.thumbFilePath,
+    this.size,
   });
 
   // 从 AssetEntity 创建

+ 238 - 12
lib/module/privacy/privacy_controller.dart

@@ -1,8 +1,13 @@
+import 'dart:ffi';
+import 'dart:math';
+
 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:clean/utils/mmkv_util.dart';
+import 'package:flutter/Material.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
@@ -19,11 +24,36 @@ import 'dart:typed_data';
 import 'dart:io';
 
 class PrivacyController extends BaseController {
+  final String isExistPasswd = "PRIVACY_EXIST_PASSWD";
+  final String isPublic = "PRIVACY_PUBLIC";
+  final String privacyPasswd = "PRIVACY_PASSWD";
+
+  // 是否存在密码
+  RxBool isPrivacyExistPasswd = false.obs;
+
+  // 是否公开
+  RxBool isPrivacyPublic = false.obs;
+
+  RxBool isConfirm = false.obs;
+
+  // 是否为编辑状态
+  RxBool isEdit = false.obs;
+
+  // 是否在重置密码
+  bool isReset = false;
+
+  // 设置密码标题
+  RxString passwordTitle = "Input password".obs;
+
+  // 密码
   late var passwordStr = "".obs;
 
+  late var newPasswordStr = "";
+
+  // 是否为解锁状态
   late var isUnlock = false.obs;
 
-  late List<AssetEntity>? imageList;
+  RxList<AssetInfo> imageList = <AssetInfo>[].obs;
 
   // 存储所有图片,按月份分组
   final assetsByMonth = <String, List<AssetInfo>>{}.obs;
@@ -35,18 +65,41 @@ class PrivacyController extends BaseController {
   int get totalAssetCount =>
       assetsByMonth.values.fold(0, (sum, list) => sum + list.length);
 
+  // 存储选中的图片ID
+  final RxSet<String> selectedAssets = <String>{}.obs;
+
+  // 是否全选
+  RxBool isAllSelected = false.obs;
+
+  // 选中图片的总容量(字节)
+  final RxInt selectedTotalSize = 0.obs;
+
   @override
   void onInit() {
     // TODO: implement onInit
     super.onInit();
 
+    isPrivacyExistPasswd.value = KVUtil.getBool(isExistPasswd, false);
+    isPrivacyPublic.value = KVUtil.getBool(isPublic, false);
+
+    isUnlock.value = isPrivacyPublic.value;
+
+    if (isPrivacyExistPasswd.value) {
+      passwordTitle.value = "Input password";
+    } else {
+      passwordTitle.value = "Create New Password";
+    }
+
     loadAssets();
   }
 
   // 加载并分组图片
   Future<void> loadAssets() async {
-    final imageList = await FileUtils.getAllAssets();
-    if (imageList.isEmpty) return;
+    imageList.value = await FileUtils.getAllAssets();
+    if (imageList.isEmpty) {
+      isEdit.value = false;
+      return;
+    }
 
     // 清空现有数据
     assetsByMonth.clear();
@@ -76,18 +129,111 @@ class PrivacyController extends BaseController {
     passwordStr.value = passwordStr.value + num;
 
     if (passwordStr.value.length == 4) {
-      if (passwordStr.value != "1234") {
-        ToastUtil.show("Input Error");
-        Future.delayed(const Duration(milliseconds: 100), () {
+      if (isReset) {
+        // 二次输入密码的情况
+        if (isConfirm.value) {
+          // 输入错误
+          if (passwordStr.value != newPasswordStr) {
+            ToastUtil.show("Input Error");
+            Future.delayed(const Duration(milliseconds: 100), () {
+              passwordStr.value = "";
+            });
+          } else {
+            isUnlock.value = true;
+            KVUtil.putString(privacyPasswd, passwordStr.value);
+            KVUtil.putBool(isExistPasswd, true);
+            KVUtil.putBool(isPublic, false);
+
+            isPrivacyExistPasswd.value = KVUtil.getBool(isExistPasswd, false);
+            isPrivacyPublic.value = KVUtil.getBool(isPublic, false);
+
+            isReset = false;
+            passwordStr.value = "";
+          }
+        } else {
+          // 第一次输入密码
+          isConfirm.value = true;
+          newPasswordStr = passwordStr.value;
+          passwordTitle.value = "Confirm Password";
+          Future.delayed(const Duration(milliseconds: 100), () {
+            passwordStr.value = "";
+          });
+        }
+        return;
+      }
+    }
+
+    if (passwordStr.value.length == 4) {
+      // 存在密码的情况
+      if (isPrivacyExistPasswd.value) {
+        String? password = KVUtil.getString(privacyPasswd, "");
+        if (passwordStr.value == password) {
+          isUnlock.value = true;
           passwordStr.value = "";
-        });
+        } else {
+          ToastUtil.show("Input Error");
+          Future.delayed(const Duration(milliseconds: 100), () {
+            passwordStr.value = "";
+          });
+        }
       } else {
-        isUnlock.value = true;
+        // 二次输入密码的情况
+        if (isConfirm.value) {
+          // 输入错误
+          if (passwordStr.value != newPasswordStr) {
+            ToastUtil.show("Input Error");
+            Future.delayed(const Duration(milliseconds: 100), () {
+              passwordStr.value = "";
+            });
+          } else {
+            isUnlock.value = true;
+            KVUtil.putString(privacyPasswd, passwordStr.value);
+            KVUtil.putBool(isExistPasswd, true);
+            KVUtil.putBool(isPublic, false);
+
+            isPrivacyExistPasswd.value = KVUtil.getBool(isExistPasswd, false);
+            isPrivacyPublic.value = KVUtil.getBool(isPublic, false);
+
+            isReset = false;
+            passwordStr.value = "";
+          }
+        } else {
+          // 第一次输入密码
+          isConfirm.value = true;
+          newPasswordStr = passwordStr.value;
+          passwordTitle.value = "Confirm Password";
+          Future.delayed(const Duration(milliseconds: 100), () {
+            passwordStr.value = "";
+          });
+        }
+        return;
       }
     }
   }
 
-  // 上传按钮点击
+  void dialogSetPassword() {
+    // 存在密码情况下
+    if (isPrivacyExistPasswd.value) {
+      isConfirm.value = false;
+      isUnlock.value = false;
+      passwordTitle.value = "Input password";
+      isReset = true;
+    } else {
+      isUnlock.value = false;
+    }
+  }
+
+// 设置公开状态
+  void setPublic() {
+    KVUtil.putString("", passwordStr.value);
+    KVUtil.putBool(isExistPasswd, false);
+    KVUtil.putBool(isPublic, true);
+
+    isPrivacyExistPasswd.value = KVUtil.getBool(isExistPasswd, false);
+    isPrivacyPublic.value = KVUtil.getBool(isPublic, false);
+  }
+
+// 上传按钮点击
   void uploadBtnClick() {
     showCupertinoModalPopup(
       context: Get.context!,
@@ -143,7 +289,7 @@ class PrivacyController extends BaseController {
     );
   }
 
-  // 保存并刷新图片列表
+// 保存并刷新图片列表
   Future<void> saveAndRefreshAssets(List<AssetEntity> assets) async {
     for (var asset in assets) {
       await FileUtils.saveAsset(asset);
@@ -152,10 +298,90 @@ class PrivacyController extends BaseController {
     loadAssets();
   }
 
-  // 开启图库
+  // 选择/取消选择图片
+  void toggleSelectAsset(String assetId) {
+    final asset = imageList.firstWhere((asset) => asset.id == assetId);
+
+    if (selectedAssets.contains(assetId)) {
+      selectedAssets.remove(assetId);
+      if (asset.size != null) {
+        selectedTotalSize.value -= asset.size!;
+      }
+    } else {
+      selectedAssets.add(assetId);
+
+      if (asset.size != null) {
+        selectedTotalSize.value += asset.size!;
+      }
+    }
+
+    // 更新全选状态
+    isAllSelected.value = selectedAssets.length == imageList.length;
+  }
+
+  // 全选/取消全选
+  void toggleSelectAll() {
+    if (isAllSelected.value) {
+      selectedAssets.clear();
+      selectedTotalSize.value = 0;
+    } else {
+      selectedAssets.addAll(imageList.map((asset) => asset.id));
+      selectedTotalSize.value = imageList.fold(
+          0, (sum, asset) => sum + (asset.size != null ? asset.size! : 0));
+    }
+    isAllSelected.value = !isAllSelected.value;
+  }
+
+  // 退出编辑模式时清空选择
+  void exitEditMode() {
+    isEdit.value = false;
+    selectedAssets.clear();
+    isAllSelected.value = false;
+    selectedTotalSize.value = 0;
+  }
+
+  // 删除文件
+  void deleteBtnClick() {
+
+    // 获取要删除的资产
+    final assetsToDelete = imageList.where(
+            (asset) => selectedAssets.contains(asset.id)
+    ).toList();
+
+    for (var asset in assetsToDelete) {
+      FileUtils.deleteAsset(asset.id.substring(0, 36));
+    }
+
+    selectedTotalSize.value = 0;
+    loadAssets();
+  }
+
+  // 格式化文件大小显示
+  String formatFileSize(int bytes) {
+    if (bytes <= 0) return "Delete";
+
+    final units = ['B', 'KB', 'MB', 'GB'];
+    int digitGroups = (log(bytes) / log(1024)).floor();
+
+    if (bytes == 0) {
+      return "Delete";
+    } else {
+      return "Delete(${(bytes / pow(1024, digitGroups)).toStringAsFixed(
+          1)} ${units[digitGroups]})";
+    }
+  }
+
+// 开启图库
   Future<void> openGallery() async {
     var status = await Permission.photos.status;
     if (status == PermissionStatus.granted) {
+      List<AssetEntity> assets = <AssetEntity>[];
+      for (var asset in imageList) {
+        var newAsset = await asset.toAssetEntity();
+        if (newAsset != null) {
+          assets.add(newAsset);
+        }
+      }
       List<AssetEntity>? pickList = await ImagePickAssets.pick();
       if (pickList != null && pickList.isNotEmpty) {
         await saveAndRefreshAssets(pickList);
@@ -165,7 +391,7 @@ class PrivacyController extends BaseController {
     }
   }
 
-  // 开启相机
+// 开启相机
   Future<void> openCamera() async {
     final entity = await CameraPicker.pickFromCamera(
       Get.context!,

+ 233 - 132
lib/module/privacy/privacy_view.dart

@@ -11,6 +11,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
 import 'package:wechat_assets_picker/wechat_assets_picker.dart';
 import '../../base/base_view.dart';
+import '../../dialog/privacy_lock_dialog.dart';
 import '../../resource/assets.gen.dart';
 import '../more/more_controller.dart';
 import 'dart:typed_data';
@@ -61,7 +62,7 @@ class PrivacyPage extends BaseView<PrivacyController> {
                   Assets.images.iconPrivacyLock
                       .image(width: 70.w, height: 70.w),
                   Text(
-                    "Input password",
+                    controller.passwordTitle.value,
                     style: TextStyle(
                       color: Colors.white,
                       fontSize: 16.sp,
@@ -248,70 +249,146 @@ class PrivacyPage extends BaseView<PrivacyController> {
     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,
+        child: Obx(() {
+          return Column(
+            mainAxisAlignment: MainAxisAlignment.start,
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              !controller.isEdit.value
+                  ? Row(
+                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                      children: [
+                        GestureDetector(
+                          onTap: () {
+                            Get.back();
+                          },
+                          child: Assets.images.iconCommonBack
+                              .image(width: 28.w, height: 28.w),
                         ),
-                      ),
+                        Obx(() {
+                          return Visibility(
+                            visible: controller.imageList.isNotEmpty,
+                            child: GestureDetector(
+                              onTap: () {
+                                controller.isEdit.value = true;
+                              },
+                              child: Container(
+                                width: 71.w,
+                                height: 30.h,
+                                decoration: BoxDecoration(
+                                  color: "#1F2D3F".color,
+                                  borderRadius: BorderRadius.all(
+                                    Radius.circular(15.h),
+                                  ),
+                                ),
+                                child: Center(
+                                  child: Text(
+                                    "Select",
+                                    style: TextStyle(
+                                      color: Colors.white,
+                                      fontSize: 14.sp,
+                                    ),
+                                  ),
+                                ),
+                              ),
+                            ),
+                          );
+                        }),
+                      ],
+                    )
+                  : Row(
+                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                      children: [
+                        GestureDetector(
+                          onTap: () {
+                            controller.isEdit.value = false;
+                          },
+                          child: Container(
+                            width: 71.w,
+                            height: 30.h,
+                            decoration: BoxDecoration(
+                              color: "#1F2D3F".color,
+                              borderRadius: BorderRadius.all(
+                                Radius.circular(15.h),
+                              ),
+                            ),
+                            child: Center(
+                              child: Text(
+                                "Cancel",
+                                style: TextStyle(
+                                  color: Colors.white,
+                                  fontSize: 14.sp,
+                                ),
+                              ),
+                            ),
+                          ),
+                        ),
+                        Obx(() {
+                          return Visibility(
+                            visible: controller.imageList.isNotEmpty,
+                            child: GestureDetector(
+                              onTap: () {
+                                controller.toggleSelectAll();
+                              },
+                              child: Text(
+                                "Select All",
+                                style: TextStyle(
+                                  color: Colors.white.withOpacity(0.65),
+                                  fontSize: 14.sp,
+                                ),
+                              ),
+                            ),
+                          );
+                        }),
+                      ],
+                    ),
+              SizedBox(
+                height: 12.h,
+              ),
+              Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                children: [
+                  Text(
+                    "Privacy Space",
+                    style: TextStyle(
+                      color: Colors.white,
+                      fontWeight: FontWeight.w700,
+                      fontSize: 24.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(
+                    onTap: () {
+                      showDialog(
+                        context: context,
+                        builder: (context) => PrivacyLockDialog(
+                          isPrivacyOn: controller.isPrivacyExistPasswd.value,
+                          onResetPassword: () {
+                            // 处理重置密码
+                            controller.dialogSetPassword();
+                            Navigator.pop(context);
+                          },
+                          onSetPublic: () {
+                            // 处理设为公开
+                            controller.setPublic();
+                            Navigator.pop(context);
+                          },
+                        ),
+                      );
+                    },
+                    child: controller.isPrivacyExistPasswd.value
+                        ? Assets.images.iconPrivacyLock
+                            .image(width: 34.w, height: 34.w)
+                        : Assets.images.iconPrivacyUnlock
+                            .image(width: 34.w, height: 34.w),
                   ),
-                ),
-                GestureDetector(
-                  child: Assets.images.iconPrivacyUnlock
-                      .image(width: 34.w, height: 34.w),
-                ),
-              ],
-            ),
-            // _buildEmptyPhotoView(context),
-            _buildPhotoView(),
-          ],
-        ),
+                ],
+              ),
+              controller.imageList.isEmpty
+                  ? _buildEmptyPhotoView(context)
+                  : _buildPhotoView(),
+            ],
+          );
+        }),
       ),
     );
   }
@@ -325,8 +402,6 @@ class PrivacyPage extends BaseView<PrivacyController> {
           SizedBox(
             height: 130.h,
           ),
-          // Image.memory(),
-          // Image.file(controller.file!),
           Assets.images.iconPrivacyEmptyImage.image(width: 70.w, height: 70.w),
           SizedBox(
             height: 22.h,
@@ -384,7 +459,8 @@ class PrivacyPage extends BaseView<PrivacyController> {
               shrinkWrap: true,
               itemCount: controller.monthCount,
               itemBuilder: (context, index) {
-                final monthAssets = ImageUtil.getMonthAssets(controller.assetsByMonth, index);
+                final monthAssets =
+                    ImageUtil.getMonthAssets(controller.assetsByMonth, index);
                 return Column(
                   mainAxisSize: MainAxisSize.min,
                   crossAxisAlignment: CrossAxisAlignment.start,
@@ -422,31 +498,68 @@ class PrivacyPage extends BaseView<PrivacyController> {
               },
             ),
           ),
-          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,
+          !controller.isEdit.value
+              ? GestureDetector(
+                  onTap: () {
+                    controller.uploadBtnClick();
+                  },
+                  child: Container(
+                    width: 328.w,
+                    height: 48.h,
+                    decoration: BoxDecoration(
+                      color: "#0279FB".color,
+                      borderRadius: BorderRadius.all(
+                        Radius.circular(10.r),
+                      ),
+                    ),
+                    child: Center(
+                      child: Text(
+                        "Upload",
+                        style: TextStyle(
+                          color: Colors.white,
+                          fontSize: 16.sp,
+                          fontWeight: FontWeight.w500,
+                        ),
+                      ),
+                    ),
                   ),
-                ),
-              ),
-            ),
-          ),
+                )
+              : GestureDetector(
+                  onTap: () {
+                    controller.deleteBtnClick();
+                  },
+                  child: Container(
+                    width: 328.w,
+                    height: 48.h,
+                    decoration: BoxDecoration(
+                      color: "#0279FB".color,
+                      borderRadius: BorderRadius.all(
+                        Radius.circular(10.r),
+                      ),
+                    ),
+                    child: Center(
+                      child: Row(
+                        mainAxisAlignment: MainAxisAlignment.center,
+                        children: [
+                          Assets.images.iconPrivacyPhotoDelete
+                              .image(width: 18.w, height: 18.h),
+                          SizedBox(
+                            width: 5.w,
+                          ),
+                          Text(
+                            controller.formatFileSize(
+                                controller.selectedTotalSize.value),
+                            style: TextStyle(
+                              color: Colors.white,
+                              fontSize: 16.sp,
+                              fontWeight: FontWeight.w500,
+                            ),
+                          ),
+                        ],
+                      ),
+                    ),
+                  ),
+                )
         ],
       ),
     );
@@ -463,60 +576,48 @@ class PrivacyPage extends BaseView<PrivacyController> {
             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) {
+                if (snapshot.data != null) {
                   return Image.memory(
                     snapshot.data!,
                     width: double.infinity,
                     height: double.infinity,
                     fit: BoxFit.cover,
+                    gaplessPlayback: true,
                   );
+                } else {
+                  return Container();
                 }
-
-                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,
-          //       ),
-          //     ),
-          //   ),
-          // ),
+          // 删除按钮
+          Visibility(
+            visible: controller.isEdit.value,
+            child: Positioned(
+              right: 4.w,
+              bottom: 4.h,
+              child: GestureDetector(
+                onTap: () {
+                  controller.toggleSelectAsset(asset.id);
+                },
+                child: Container(
+                  child: controller.selectedAssets.contains(asset.id)
+                      ? Center(
+                          child: Assets.images.iconSelected.image(
+                            width: 16.w,
+                            height: 16.h,
+                          ),
+                        )
+                      : Center(
+                          child: Assets.images.iconUnselected.image(
+                            width: 16.w,
+                            height: 16.h,
+                          ),
+                        ),
+                ),
+              ),
+            ),
+          ),
         ],
       ),
     );

+ 18 - 6
lib/utils/file_utils.dart

@@ -1,6 +1,7 @@
 import 'dart:io';
 import 'dart:convert';
 import 'dart:typed_data';
+import 'package:clean/utils/image_util.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:photo_manager/photo_manager.dart';
 
@@ -22,7 +23,7 @@ class FileUtils {
   static Future<AssetEntity?> saveAsset(AssetEntity asset) async {
     try {
       final assetPath = await getAssetPath();
-      final title = asset.createDateSecond;
+      final title = asset.id.substring(0, 36);
       final assetFile = File('$assetPath/$title.json');
 
       // // 将 AssetEntity 转换为 AssetInfo 后再序列化
@@ -71,7 +72,7 @@ class FileUtils {
   }
 
   /// 从本地读取缩略图数据
-  static Future<Uint8List?> getThumbData(int assetId) async {
+  static Future<Uint8List?> getThumbData(String assetId) async {
     try {
       final assetPath = await getAssetPath();
       final thumbFile = File('$assetPath/${assetId}thumb.jpg');
@@ -120,11 +121,11 @@ class FileUtils {
           final jsonStr = await entity.readAsString();
           final json = jsonDecode(jsonStr);
           final assetInfo = AssetInfo.fromJson(json);
+          File? file = await ImageUtil.getImageFile(assetInfo);
+          if (file != null) {
+            assetInfo.size = await FileUtils.getFileSize(file);
+          }
           assets.add(assetInfo);
-          // final asset = await assetInfo.toAssetEntity();
-          // if (asset != null) {
-          //   assets.add(asset);
-          // }
         }
       }
 
@@ -149,4 +150,15 @@ class FileUtils {
       return false;
     }
   }
+
+  // 获取文件大小
+  static Future<int> getFileSize(File file) async {
+    try {
+      final bytes = await file.length();
+      return bytes;
+    } catch (e) {
+      print('获取文件大小失败: $e');
+      return 0;
+    }
+  }
 }

+ 3 - 3
lib/utils/image_util.dart

@@ -50,7 +50,7 @@ class ImageUtil {
   static Future<Uint8List?> getImageThumbnail(AssetInfo asset) async {
     try {
       // 先尝试从本地读取缩略图
-      final localThumb = await FileUtils.getThumbData(asset.createDateSecond!);
+      final localThumb = await FileUtils.getThumbData(asset.id.substring(0, 36));
       if (localThumb != null) {
         return localThumb;
       }
@@ -68,13 +68,13 @@ class ImageUtil {
     try {
       // 先尝试从本地读取
       final assetPath = await FileUtils.getAssetPath();
-      final localFile = File('$assetPath/${asset.createDateSecond}.jpg');
+      final localFile = File('$assetPath/${asset.id.substring(0, 36)}.jpg');
       if (await localFile.exists()) {
         return localFile;
       }
 
       // 如果本地没有,则从 AssetEntity 获取
-      return await asset.file;
+      return asset.file;
     } catch (e) {
       print('获取图片文件失败: $e');
       return null;

+ 52 - 0
lib/utils/mmkv_util.dart

@@ -0,0 +1,52 @@
+import 'package:mmkv/mmkv.dart';
+
+class KVUtil {
+  KVUtil._();
+
+  static MMKV? _mmkv;
+
+  static init() async {
+    if (_mmkv != null) {
+      return;
+    }
+    await MMKV.initialize();
+    _mmkv = MMKV.defaultMMKV();
+  }
+
+  static void putString(String key, String? value) {
+    _getMMKV().encodeString(key, value);
+  }
+
+  static String? getString(String key, String? defaultValue) {
+    return _getMMKV().decodeString(key) ?? defaultValue;
+  }
+
+  static void putInt(String key, int value) {
+    _getMMKV().encodeInt(key, value);
+  }
+
+  static int getInt(String key, int defaultValue) {
+    return _getMMKV().decodeInt(key, defaultValue: defaultValue);
+  }
+
+  static void putBool(String key, bool value) {
+    _getMMKV().encodeBool(key, value);
+  }
+
+  static bool getBool(String key, bool defaultValue) {
+    return _getMMKV().decodeBool(key, defaultValue: defaultValue);
+  }
+
+  static void putDouble(String key, double value) {
+    _getMMKV().encodeDouble(key, value);
+  }
+
+  static double getDouble(String key, double defaultValue) {
+    return _getMMKV().decodeDouble(key, defaultValue: defaultValue);
+  }
+
+  static MMKV _getMMKV() {
+    _mmkv ??= MMKV.defaultMMKV();
+    return _mmkv!;
+  }
+}

+ 40 - 0
pubspec.lock

@@ -649,6 +649,46 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.0"
+  mmkv:
+    dependency: "direct main"
+    description:
+      name: mmkv
+      sha256: bbef7d75eac321a32892e8b3d13408cada01f6363e29d4a539e6742eaeb44b68
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.3.11"
+  mmkv_android:
+    dependency: transitive
+    description:
+      name: mmkv_android
+      sha256: "4a8bcef2a405d286ce04ab226c8b506776217f89b64476a10bdd969be3a44b35"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.9"
+  mmkv_ios:
+    dependency: transitive
+    description:
+      name: mmkv_ios
+      sha256: d303510b1d5584fac531f271b2229cfbdd06dcb068b1e6f3c2863ac7c031715e
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.8"
+  mmkv_ohos:
+    dependency: transitive
+    description:
+      name: mmkv_ohos
+      sha256: "5b02cfe7774a2fb7cf2edeef4afbdcf088b65b0b44cc71b97225eb4f19e510fb"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.4"
+  mmkv_platform_interface:
+    dependency: transitive
+    description:
+      name: mmkv_platform_interface
+      sha256: "7e120ee1d8cfbbeb054a49e7ed6b889e362208815766f64c1c9c190e2ce15047"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.3"
   nested:
     dependency: transitive
     description:

+ 3 - 0
pubspec.yaml

@@ -82,6 +82,9 @@ dependencies:
   #  路径提供程序
   path_provider: ^2.1.5
 
+  #tencent mmkv
+  mmkv: ^1.3.9
+
   # 存储空间
   disk_space: ^0.2.1