云天逵 1 год назад
Родитель
Сommit
0986efb3cd

+ 60 - 27
lib/base/base_photo_controller.dart

@@ -3,6 +3,7 @@ import 'package:clean/data/repositories/user_repository.dart';
 import 'package:clean/dialog/photo_delete_finish_dialog.dart';
 import 'package:clean/dialog/photo_deleting_dialog.dart';
 import 'package:clean/module/store/store_view.dart';
+import 'package:flutter/cupertino.dart';
 import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
 import 'package:get/get.dart';
 import 'package:wechat_assets_picker/wechat_assets_picker.dart';
@@ -18,47 +19,73 @@ abstract class BasePhotoController extends BaseController {
   RxInt selectedFileCount = 0.obs;
   final RxSet<String> selectedPhotosIds = <String>{}.obs;
 
+
   // 将selectedFilesSize转成String类型,然后单位转换,如果超过1MB,则转成MB,超过1GB,则转成GB,否则KB
   String get selectedFilesSizeString {
-    if (selectedFilesSize.value > 1024) {
-      return "${(selectedFilesSize.value / 1024).toStringAsFixed(2)}MB";
-    } else if (selectedFilesSize.value > 1024 * 1024) {
-      return "${(selectedFilesSize.value / 1024 / 1024).toStringAsFixed(2)}GB";
-    } else {
-      return "${(selectedFilesSize.value).toStringAsFixed(2)}KB";
+    print("BasePhotoController  selectedFilesSize.value ${selectedFilesSize.value}");
+    final double sizeInKB = selectedFilesSize.value;
+
+    if (sizeInKB >= 1024 * 1024) {  // 先检查最大单位(GB)
+      return "${(sizeInKB / (1024 * 1024)).toStringAsFixed(2)}GB";
+    } else if (sizeInKB >= 1024) {  // 然后检查MB
+      return "${(sizeInKB / 1024).toStringAsFixed(2)}MB";
+    } else {  // 最后是KB
+      return "${sizeInKB.toStringAsFixed(2)}KB";
     }
   }
 
-  // 获取选中的文件大小
   Future<void> updateSelectedFilesSize() async {
-    // 如果没有选中的文件,直接返回
-    if (selectedFileCount.value == 0) {
+
+
+    if (selectedPhotosIds.isEmpty) {
+
       selectedFilesSize.value = 0;
+
+      print("BasePhotoController updateSelectedFilesSize isEmpty");
+
       return;
     }
+
+    print("BasePhotoController updateSelectedFilesSize");
+
     double totalSize = 0;
-    for (var group in photoGroups) {
-      for (int i = 0; i < group.images.length; i++) {
-        if (group.selectedImages[i]) {
-          // 检查缓存
-          final assetId = group.images[i].id;
-          if (ImagePickerUtil.fileSizeCache.containsKey(assetId)) {
-            totalSize += ImagePickerUtil.fileSizeCache[assetId]!;
-          } else {
-            final file = await group.images[i].file;
-            if (file != null) {
-              final size = (await file.length()) / 1024; // 转换为KB
-              totalSize += size;
-              ImagePickerUtil.fileSizeCache[assetId] = size;
-            }
-          }
-        }
-      }
+
+    // 创建一个 Future 列表,用于并行获取文件大小
+    List<Future<double>> sizeFutures =
+        selectedPhotosIds.map((id) => getFileSize(id)).toList();
+
+    // 等待所有文件大小计算完成
+    List<double> sizes = await Future.wait(sizeFutures);
+    // 计算总大小
+    totalSize = sizes.fold(0, (sum, size) => sum + size);
+    if (selectedPhotosIds.isEmpty) {
+      selectedFilesSize.value = 0;
+      return;
     }
-    selectedFilesSize.value = totalSize;
+    selectedFilesSize.value = totalSize; // 更新总大小
     PhotoManager.clearFileCache();
   }
 
+  Future<double> getFileSize(String assetId) async {
+    // 先检查缓存
+    if (ImagePickerUtil.fileSizeCache.containsKey(assetId)) {
+      return ImagePickerUtil.fileSizeCache[assetId]!;
+    }
+
+    final entity = await AssetEntity.fromId(assetId);
+    if (entity != null) {
+      final file = await entity.file;
+      if (file != null) {
+        double size = await file.length() / 1024; // 转换为 KB
+        ImagePickerUtil.fileSizeCache[assetId] = size; // 缓存文件大小
+        file.delete();
+        return size;
+      }
+    }
+    return 0;
+  }
+
+
   // 切换图片选中状态
   Future<void> toggleImageSelection(
       List<AssetEntity> groupTitle, int imageIndex) async {
@@ -117,6 +144,7 @@ abstract class BasePhotoController extends BaseController {
       updateSelectedPhotosIds(image.id, newValue);
     }
     selectedFileCount.value = selectedPhotosIds.length;
+
     updateSelectedFilesSize();
   }
 
@@ -181,10 +209,15 @@ abstract class BasePhotoController extends BaseController {
             group.images.removeWhere(
                 (element) => selectedPhotosIds.contains(element.id));
           }
+
           ImagePickerUtil.updatePhotoGroupDate(
               getPhotosType(), selectedPhotosIds);
 
           selectedPhotosIds.clear();
+          selectedFileCount.value = selectedPhotosIds.length;
+          print(
+              "BasePhotoController clickDelete selectedPhotosIds $selectedPhotosIds");
+          updateSelectedFilesSize();
 
           ToastUtil.show("Delete success");
           Future.delayed(Duration(seconds: 2), () {

+ 138 - 11
lib/module/calendar/calendar_controller.dart

@@ -1,18 +1,22 @@
+import 'package:clean/module/calendar/calendar_state.dart';
+import 'package:clean/module/calendar/preview/calendar_preview_view.dart';
 import 'package:clean/module/people_photo/photo_group.dart';
+import 'package:flutter/Material.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:intl/intl.dart';
 import 'package:wechat_assets_picker/wechat_assets_picker.dart';
 
 import '../../base/base_controller.dart';
 import 'package:get/get.dart';
 
-import '../../utils/file_utils.dart';
-import '../../utils/image_util.dart';
 import '../image_picker/image_picker_util.dart';
 import 'calendar_month_view.dart';
 
 class CalendarController extends BaseController {
-  RxList<PhotoGroup> monthlyAlbums = <PhotoGroup>[].obs;
-  RxList<AssetEntity> imageList = <AssetEntity>[].obs;
+  final RxList<PhotoGroup> monthlyAlbums = CalendarState.monthlyAlbums;
+  final RxList<AssetEntity> imageList = <AssetEntity>[].obs;
+  final Rx<SortType> currentSortType = SortType.latest.obs; // 当前排序类型
 
   @override
   void onInit() {
@@ -22,9 +26,8 @@ class CalendarController extends BaseController {
 
   // 加载并分组图片
   Future<void> loadAssets() async {
-    final List<AssetEntity> result = await ImagePickerUtil.loadAssetsAndroid();
+    final List<AssetEntity> result = await ImagePickerUtil.loadAssets();
     result.sort((a, b) => b.createDateTime.compareTo(a.createDateTime));
-
     imageList.value = result;
     updateMonthlyAssets();
   }
@@ -33,17 +36,18 @@ class CalendarController extends BaseController {
   void updateMonthlyAssets() {
     Map<String, List<AssetEntity>> groupedAssets = {};
 
+    // 按月份分组照片
     for (var asset in imageList) {
       final monthKey = DateFormat('yyyy-MM').format(asset.createDateTime);
       groupedAssets.putIfAbsent(monthKey, () => []).add(asset);
     }
 
-
     monthlyAlbums.clear();
     groupedAssets.forEach((month, assets) {
       assets
           .sort((a, b) => b.createDateTime.compareTo(a.createDateTime)); // 时间排序
-      monthlyAlbums.add(PhotoGroup(month: month, images: assets, isSelected: false));
+      monthlyAlbums
+          .add(PhotoGroup(month: month, images: assets, isSelected: false));
     });
 
     // 打印结果
@@ -52,17 +56,140 @@ class CalendarController extends BaseController {
     }
   }
 
+  // 点击月份卡片,跳转到对应月份的照片详情页
   void clickMonthCard(PhotoGroup photoGroup) {
     print("clickMonthCard");
     CalendarMonthPage.start(photoGroup: photoGroup);
   }
 
-  void clickImage(){
-    print("clickImage");
+  // 点击图片,查看该图片详情
+  void clickImage(PhotoGroup photoGroup, int imageIndex) {
+    print("CalendarController clickImage");
+
+    CalendarPreviewPage.start(
+        photoGroup: photoGroup,
+        currentImageId: photoGroup.images[imageIndex].id);
+  }
+
+  // 排序照片列表
+  void sortAssets(SortType sortType) {
+    currentSortType.value = sortType; // 更新当前排序类型
+    switch (sortType) {
+      case SortType.latest:
+        imageList.sort((a, b) => b.createDateTime.compareTo(a.createDateTime));
+        break;
+      case SortType.oldest:
+        imageList.sort((a, b) => a.createDateTime.compareTo(b.createDateTime));
+        break;
+      // case SortType.largeSize:
+      //   break;
+      case SortType.mostQuantity:
+        monthlyAlbums
+            .sort((a, b) => b.images.length.compareTo(a.images.length));
+        imageList.clear();
+        for (var album in monthlyAlbums) {
+          imageList.addAll(album.images); // 将每个月的照片重新按顺序添加到 imageList
+        }
+        break;
+    }
+    updateMonthlyAssets(); // 排序后更新分组
   }
 
 
+  // 显示排序弹窗
+// 显示排序弹窗
+  void clickSort() {
+    showCupertinoModalPopup(
+      context: Get.context!,
+      builder: (context) {
+        return Container(
+          width: 360.w,
+          decoration: ShapeDecoration(
+            color: Color(0xFF131C27),
+            shape: RoundedRectangleBorder(
+              borderRadius: BorderRadius.only(
+                topLeft: Radius.circular(44.r),
+                topRight: Radius.circular(44.r),
+              ),
+            ),
+          ),
+          child: Column(
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              SizedBox(height: 10.h),
+              Container(
+                width: 57.w,
+                height: 4.h,
+                decoration: ShapeDecoration(
+                  color: Color(0xFF242D38),
+                  shape: RoundedRectangleBorder(
+                      borderRadius: BorderRadius.circular(2)),
+                ),
+              ),
+              SizedBox(height: 19.h),
+              Text(
+                'Sort By',
+                textAlign: TextAlign.center,
+                style: TextStyle(
+                  color: Colors.white,
+                  fontSize: 16.sp,
+                  fontWeight: FontWeight.w700,
+                ),
+              ),
+              SizedBox(height: 14.h),
+              // Latest
+              sortOption(SortType.latest, 'Latest'),
+              SizedBox(height: 14.h),
+              // Oldest
+              sortOption(SortType.oldest, 'Oldest'),
+              SizedBox(height: 14.h),
+              // Large Size
+              // sortOption(SortType.largeSize, 'Large Size'),
+              // SizedBox(height: 14.h),
+              // Most Quantity
+              sortOption(SortType.mostQuantity, 'Most Quantity'),
+              SizedBox(height: 55.h),
+            ],
+          ),
+        );
+      },
+    );
+  }
 
+  Widget sortOption(SortType type, String title) {
+    bool isSelected = currentSortType.value == type; // 判断是否是选中的排序类型
+    return GestureDetector(
+      onTap: () {
+        Navigator.pop(Get.context!);
+        sortAssets(type);
+      },
+      child: Container(
+        width: 300.w,
+        height: 50.h,
+        alignment: Alignment.center,
+        decoration: ShapeDecoration(
+          color: isSelected ? Color(0x1C0279FB) : Color(0xFF242D38),
+          shape: RoundedRectangleBorder(
+            borderRadius: BorderRadius.circular(14.r),
+          ),
+        ),
+        child: Text(
+          title,
+          style: TextStyle(
+            color: isSelected ? Colors.white : Colors.white.withValues(alpha: 0.8),
+            fontSize: isSelected ? 16.sp : 14.sp,
+            fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500,
+          ),
+        ),
+      ),
+    );
+  }
 }
 
-
+// 排序类型
+enum SortType {
+  latest, // 最新
+  oldest, // 最旧
+  // largeSize, // 按文件组大小排序
+  mostQuantity // 按每个月照片数量排序
+}

+ 128 - 71
lib/module/calendar/calendar_month_controller.dart

@@ -1,16 +1,24 @@
 import 'package:clean/base/base_controller.dart';
+import 'package:clean/module/calendar/calendar_view.dart';
+import 'package:clean/module/calendar/preview/calendar_preview_view.dart';
+import 'package:flutter/Material.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
 import 'package:get/get.dart';
 import 'package:wechat_assets_picker/wechat_assets_picker.dart';
 
+import '../../data/repositories/user_repository.dart';
+import '../../dialog/photo_delete_finish_dialog.dart';
+import '../../dialog/photo_deleting_dialog.dart';
+import '../../utils/toast_util.dart';
 import '../image_picker/image_picker_util.dart';
 import '../people_photo/photo_group.dart';
+import '../store/store_view.dart';
 
 class CalendarMonthController extends BaseController {
   late final Rx<PhotoGroup> photoGroup =
       PhotoGroup(isSelected: false, images: []).obs;
-  final RxDouble selectedFilesSize = 0.0.obs;
-  RxInt selectedFileCount = 0.obs;
-  final RxSet<String> selectedPhotosIds = <String>{}.obs;
+
+
 
   // 是否是选择模式
   RxBool isSelectMode = false.obs;
@@ -23,7 +31,7 @@ class CalendarMonthController extends BaseController {
 
   void _getArgs() {
     final parameters = Get.arguments;
-    photoGroup.value = parameters?['PhotoGroup'] as PhotoGroup;
+    photoGroup.value = parameters?['photoGroup'];
   }
 
   clickBack() {
@@ -36,104 +44,153 @@ class CalendarMonthController extends BaseController {
     final newValue = !photoGroup.value.isSelected.value;
     photoGroup.value.toggleSelectAll(newValue);
 
-    for (var image in photoGroup.value.images) {
+
+    // 更新选中状态
+    for (var image in imagesList) {
       updateSelectedPhotosIds(image.id, newValue);
     }
-    selectedFileCount.value = selectedPhotosIds.length;
     updateSelectedFilesSize();
   }
 
+  clickDelete() async {
+    debugPrint('CalendarMonthController clickDelete');
+    if (userRepository.isVip()) {
+
+    if (photoGroup.value.selectedPhotosIds.isNotEmpty) {
+      photoDeletingDialog();
+      // 获取要删除的资产
+      final assetsToDelete = photoGroup.value.images
+          .where(
+            (asset) => photoGroup.value.selectedPhotosIds.contains(asset.id),
+      )
+          .toList();
+
+      // 调用方法会返回被删除的资源,如果全部失败会返回空列表。
+      final List<String> result = await PhotoManager.editor.deleteWithIds(
+        assetsToDelete.map((e) => e.id).toList(),
+      );
+
+
+      //   比较result和selectedPhotosIds,如果result和selectedPhotosIds相等,说明删除成功,走下面的逻辑
+      // 如果不相等,说明有删除失败的,走else逻辑
+      if (result.length == photoGroup.value.selectedPhotosIds.length) {
+
+        debugPrint("CalendarMonthController delete ${photoGroup.value.selectedPhotosIds}");
+        ImagePickerUtil.updatePhotoData(
+            photoGroup.value.selectedPhotosIds);
+        cleanSelections();
+        ToastUtil.show('Delete success');
+        Future.delayed(Duration(seconds: 2), () {
+          SmartDialog.dismiss(tag: 'photoDeletingDialog');
+          photoDeleteFinishDialog();
+        });
+      } else {
+        SmartDialog.dismiss(tag: 'photoDeletingDialog');
+        // 删除失败
+        ToastUtil.show("Delete failed");
+      }
+    }
+    } else {
+      StorePage.start();
+    }
+  }
+  //删除成功清除选中的图片
+  void cleanSelections() async {
+    photoGroup.value.images
+        .removeWhere((element) => photoGroup.value.selectedPhotosIds.contains(element.id));
+
+
+    photoGroup.value.selectedPhotosIds.clear();
+
+
+    if (photoGroup.value.images.isEmpty) {
+      return;
+    }
+    updateSelectedFilesSize();
+
+  }
+
   Future<void> updateSelectedFilesSize() async {
     // 如果没有选中的文件,直接返回
-    if (selectedFileCount.value == 0) {
-      selectedFilesSize.value = 0;
+    if (photoGroup.value.selectedCount == 0) {
+      photoGroup.value.selectedTotalSize.value = 0;
       return;
     }
+
     double totalSize = 0;
+    final selectedImages = photoGroup.value.images;
 
-    for (int i = 0; i < photoGroup.value.images.length; i++) {
-      if (photoGroup.value.selectedImages[i]) {
-        // 检查缓存
-        final assetId = photoGroup.value.images[i].id;
-        if (ImagePickerUtil.fileSizeCache.containsKey(assetId)) {
-          totalSize += ImagePickerUtil.fileSizeCache[assetId]!;
-        } else {
-          final file = await photoGroup.value.images[i].file;
-          if (file != null) {
-            final size = (await file.length()) / 1024; // 转换为KB
-            totalSize += size;
-            ImagePickerUtil.fileSizeCache[assetId] = size;
-          }
+    // 获取选中图片的大小(异步并行执行)
+    final futures = photoGroup.value.selectedPhotosIds.map((assetId) async {
+      if (ImagePickerUtil.fileSizeCache.containsKey(assetId)) {
+        totalSize += ImagePickerUtil.fileSizeCache[assetId]!;
+      } else {
+        final image = selectedImages.firstWhere((image) => image.id == assetId);
+        final file = await image.file;
+        if (file != null) {
+          final size = (await file.length()) / 1024; // 转换为KB
+          totalSize += size;
+          ImagePickerUtil.fileSizeCache[assetId] = size;
+          file.delete();
         }
       }
-    }
+    }).toList();
+
+    // 等待所有异步操作完成
+    await Future.wait(futures);
+
+    // 更新选中文件的总大小
+    photoGroup.value.selectedTotalSize.value = totalSize;
+
+    // 打印调试信息
+    print("selectedFilesSize: ${photoGroup.value.selectedTotalSize.value}");
+
 
-    selectedFilesSize.value = totalSize;
-    print("selectedFilesSize: $selectedFilesSize");
-    PhotoManager.clearFileCache();
   }
 
   void updateSelectedPhotosIds(String photoId, bool isSelected) {
     if (isSelected) {
-      selectedPhotosIds.add(photoId);
+      if (!photoGroup.value.selectedPhotosIds.contains(photoId)) {
+        photoGroup.value.selectedPhotosIds.add(photoId);
+      }
     } else {
-      selectedPhotosIds.remove(photoId);
+      photoGroup.value.selectedPhotosIds.remove(photoId);
     }
+    photoGroup.value.isSelected.value =
+        photoGroup.value.selectedPhotosIds.length == photoGroup.value.images.length;
   }
-
-  clickImage(List<AssetEntity> images, int imageIndex) {
+  clickImage(int imageIndex) {
     print("CalendarMonthController clickImage");
-  }
 
-  Future<void> toggleImageSelection(
-      List<AssetEntity> groupTitle, int imageIndex) async {
-    print("BasePhotoController toggleImageSelection");
+    CalendarPreviewPage.start(
+        photoGroup: photoGroup.value,
+        currentImageId: photoGroup.value.images[imageIndex].id);
+  }
 
+  Future<void> toggleImageSelection(int imageIndex) async {
+    print("CalendarMonthController toggleImageSelection");
     final image = photoGroup.value.images[imageIndex];
-    final selected = !photoGroup.value.selectedImages[imageIndex];
+    final photoId = image.id;
 
-    photoGroup.value.selectedImages[imageIndex] = selected;
-    updateSelectedPhotosIds(image.id, selected);
+    // 反转选择状态
+    final isNowSelected = !photoGroup.value.selectedPhotosIds.contains(photoId);
+    updateSelectedPhotosIds(photoId, isNowSelected);
 
-    photoGroup.value.isSelected.value =
-        photoGroup.value.selectedImages.every((selected) => selected);
-
-    selectedFileCount.value = selectedPhotosIds.length;
-
-    // 更新文件大小
-    if (selected) {
-      // 如果选中,增加文件大小
-      if (ImagePickerUtil.fileSizeCache.containsKey(image.id)) {
-        selectedFilesSize.value += ImagePickerUtil.fileSizeCache[image.id]!;
-      } else {
-        final file = await image.file;
-        if (file != null) {
-          selectedFilesSize.value += (await file.length()) / 1024; // 转换为KB
-        }
-      }
-    } else {
-      // 如果取消选中,减少文件大小
-      if (ImagePickerUtil.fileSizeCache.containsKey(image.id)) {
-        selectedFilesSize.value -= ImagePickerUtil.fileSizeCache[image.id]!;
-      } else {
-        final file = await image.file;
-        if (file != null) {
-          selectedFilesSize.value -= (await file.length()) / 1024; // 转换为KB
-        }
-      }
-    }
+    updateSelectedFilesSize();
 
-    PhotoManager.clearFileCache();
   }
 
-  // 将selectedFilesSize转成String类型,然后单位转换,如果超过1MB,则转成MB,超过1GB,则转成GB,否则KB
+
   String get selectedFilesSizeString {
-    if (selectedFilesSize.value > 1024) {
-      return "${(selectedFilesSize.value / 1024).toStringAsFixed(2)}MB";
-    } else if (selectedFilesSize.value > 1024 * 1024) {
-      return "${(selectedFilesSize.value / 1024 / 1024).toStringAsFixed(2)}GB";
-    } else {
-      return "${(selectedFilesSize.value).toStringAsFixed(2)}KB";
+
+    final double sizeInKB = photoGroup.value.selectedTotalSize.value;
+
+    if (sizeInKB >= 1024 * 1024) {  // 先检查最大单位(GB)
+      return "${(sizeInKB / (1024 * 1024)).toStringAsFixed(2)}GB";
+    } else if (sizeInKB >= 1024) {  // 然后检查MB
+      return "${(sizeInKB / 1024).toStringAsFixed(2)}MB";
+    } else {  // 最后是KB
+      return "${sizeInKB.toStringAsFixed(2)}KB";
     }
   }
 }

+ 148 - 143
lib/module/calendar/calendar_month_view.dart

@@ -11,11 +11,11 @@ import '../people_photo/photo_group.dart';
 import 'calendar_state.dart';
 
 class CalendarMonthPage extends BasePage<CalendarMonthController> {
-  const CalendarMonthPage({Key? key}) : super(key: key);
+  const CalendarMonthPage({Key? key});
 
   static void start({required PhotoGroup photoGroup}) {
     Get.toNamed(RoutePath.calendarMonth, arguments: {
-      "PhotoGroup": photoGroup,
+      "photoGroup": photoGroup,
     });
   }
 
@@ -50,29 +50,33 @@ class CalendarMonthPage extends BasePage<CalendarMonthController> {
               return Column(
                 children: [
                   _titleCard(),
-                  Obx(() {
-                    return Expanded(
-                      child: GridView.builder(
+                  Expanded(
+                    child: Obx(() {
+                      debugPrint(
+                          "CalendarMonthPage buildBody ${controller.photoGroup
+                              .value.images.length}");
+                      return GridView.builder(
                           padding: EdgeInsets.symmetric(horizontal: 16.w),
                           scrollDirection: Axis.vertical,
                           // 设置为垂直方向滚动
                           physics: BouncingScrollPhysics(),
-                          gridDelegate:
-                              SliverGridDelegateWithFixedCrossAxisCount(
+                          cacheExtent: 1000,
+                          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                             crossAxisCount: 3, // 每行显示 3 个元素
                             mainAxisSpacing: 8.w, // 垂直间距
                             crossAxisSpacing: 8.h, // 水平间距
                           ),
                           itemCount: controller.photoGroup.value.images.length,
                           itemBuilder: _buildPhotoItem(
-                              controller.photoGroup.value.images)),
-                    );
-                  }),
+                              controller.photoGroup.value.images));
+                    }),
+                  ),
                   Obx(() {
                     return controller.isSelectMode.value
-                        ? controller.selectedPhotosIds.isNotEmpty
-                            ? _bottomBarCard()
-                            : Container()
+                        ? controller
+                        .photoGroup.value.selectedPhotosIds.isNotEmpty
+                        ? _bottomBarCard()
+                        : Container()
                         : Container();
                   }),
                   SizedBox(height: 8.h),
@@ -101,166 +105,166 @@ class CalendarMonthPage extends BasePage<CalendarMonthController> {
               children: [
                 controller.isSelectMode.value
                     ? GestureDetector(
-                        onTap: () {
-                          controller.isSelectMode.value = false;
-                        },
-                        child: Container(
-                          width: 71.w,
-                          height: 30.h,
-                          decoration: ShapeDecoration(
-                            color: Color(0xFF2A3E55),
-                            shape: RoundedRectangleBorder(
-                              borderRadius: BorderRadius.circular(83.r),
-                            ),
-                          ),
-                          child: Center(
-                            child: Text(
-                              'Cancel',
-                              style: TextStyle(
-                                color: Colors.white,
-                                fontSize: 14.sp,
-                                fontWeight: FontWeight.w400,
-                              ),
-                            ),
+                    onTap: () {
+                      controller.isSelectMode.value = false;
+                    },
+                    child: Container(
+                      width: 71.w,
+                      height: 30.h,
+                      decoration: ShapeDecoration(
+                        color: Color(0xFF2A3E55),
+                        shape: RoundedRectangleBorder(
+                          borderRadius: BorderRadius.circular(83.r),
+                        ),
+                      ),
+                      child: Center(
+                        child: Text(
+                          'Cancel',
+                          style: TextStyle(
+                            color: Colors.white,
+                            fontSize: 14.sp,
+                            fontWeight: FontWeight.w400,
                           ),
-                        ))
-                    : GestureDetector(
-                        onTap: controller.clickBack,
-                        child: Assets.images.iconBackArrow.image(
-                          width: 28.w,
-                          height: 28.h,
                         ),
                       ),
+                    ))
+                    : GestureDetector(
+                  onTap: controller.clickBack,
+                  child: Assets.images.iconBackArrow.image(
+                    width: 28.w,
+                    height: 28.h,
+                  ),
+                ),
                 controller.photoGroup.value.images.isEmpty
                     ? Container()
                     : GestureDetector(
-                        onTap: () => controller.isSelectMode.value
-                            ? controller.toggleGroupSelection(
-                                controller.photoGroup.value.images)
-                            : null,
-                        child: Obx(() => controller.isSelectMode.value
-                            ? Text(
-                                controller.photoGroup.value.isSelected.value
-                                    ? 'Deselect All'
-                                    : 'Select All',
-                                style: TextStyle(
-                                  color: Colors.white.withValues(alpha: 0.7),
-                                  fontSize: 14.sp,
-                                  fontWeight: FontWeight.w400,
-                                ),
-                              )
-                            : GestureDetector(
-                                onTap: () {
-                                  controller.isSelectMode.value = true;
-                                },
-                                child: Container(
-                                  width: 71.w,
-                                  height: 30.h,
-                                  alignment: Alignment.center,
-                                  decoration: ShapeDecoration(
-                                    color: Color(0xFF1F2D3F),
-                                    shape: RoundedRectangleBorder(
-                                      borderRadius: BorderRadius.circular(83.r),
-                                    ),
-                                  ),
-                                  child: Text(
-                                    'Select',
-                                    style: TextStyle(
-                                      color: Colors.white,
-                                      fontSize: 14.sp,
-                                      fontWeight: FontWeight.w400,
-                                    ),
-                                  ),
-                                ),
-                              )),
+                  onTap: () =>
+                  controller.isSelectMode.value
+                      ? controller.toggleGroupSelection(
+                      controller.photoGroup.value.images)
+                      : null,
+                  child: Obx(() =>
+                  controller.isSelectMode.value
+                      ? Text(
+                    controller.photoGroup.value.isSelected.value
+                        ? 'Deselect All'
+                        : 'Select All',
+                    style: TextStyle(
+                      color: Colors.white.withValues(alpha: 0.7),
+                      fontSize: 14.sp,
+                      fontWeight: FontWeight.w400,
+                    ),
+                  )
+                      : GestureDetector(
+                    onTap: () {
+                      controller.isSelectMode.value = true;
+                    },
+                    child: Container(
+                      width: 71.w,
+                      height: 30.h,
+                      alignment: Alignment.center,
+                      decoration: ShapeDecoration(
+                        color: Color(0xFF1F2D3F),
+                        shape: RoundedRectangleBorder(
+                          borderRadius: BorderRadius.circular(83.r),
+                        ),
                       ),
+                      child: Text(
+                        'Select',
+                        style: TextStyle(
+                          color: Colors.white,
+                          fontSize: 14.sp,
+                          fontWeight: FontWeight.w400,
+                        ),
+                      ),
+                    ),
+                  )),
+                ),
               ],
             );
           }),
           controller.photoGroup.value.images.isEmpty
               ? Container()
               : Column(
-                  children: [
-                    SizedBox(height: 12.h),
-                    Text(
-                      CalendarState.formatMonth(
-                          controller.photoGroup.value.month ?? ""),
-                      style: TextStyle(
-                        color: Colors.white,
-                        fontSize: 24.sp,
-                        fontWeight: FontWeight.w700,
-                      ),
-                    ),
-                    SizedBox(height: 20.h),
-                  ],
+            children: [
+              SizedBox(height: 12.h),
+              Text(
+                CalendarState.formatMonth(
+                    controller.photoGroup.value.month ?? ""),
+                style: TextStyle(
+                  color: Colors.white,
+                  fontSize: 24.sp,
+                  fontWeight: FontWeight.w700,
                 ),
+              ),
+              SizedBox(height: 20.h),
+            ],
+          ),
         ],
       ),
     );
   }
 
   Widget Function(BuildContext, int) _buildPhotoItem(
-          List<AssetEntity> images) =>
-      (context, index) {
-        final group = controller.photoGroup.value;
-
-        return GestureDetector(
-          onTap: () => controller.clickImage(images, index),
-          child: Obx(() {
-            final isSelected = group.selectedImages[index];
-            return Stack(
-              children: [
-                Container(
-                  width: 104.w,
-                  height: 104.w,
-                  decoration: ShapeDecoration(
-                    color: Colors.white.withValues(alpha: 0.12),
-                    shape: RoundedRectangleBorder(
-                      borderRadius: BorderRadius.circular(9.27.sp),
-                    ),
-                    image: DecorationImage(
-                      image: AssetEntityImageProvider(
-                        group.images[index],
-                        thumbnailSize: const ThumbnailSize.square(300),
-                        isOriginal: false,
+      List<AssetEntity> images) =>
+          (context, index) {
+        return Obx(() {
+          final photoId = controller.photoGroup.value.images[index].id;
+          final isSelected =
+          controller.photoGroup.value.selectedPhotosIds.contains(photoId);
+          return GestureDetector(
+              onTap: () => controller.clickImage(index),
+              child: Stack(
+                children: [
+                  Container(
+                    width: 104.w,
+                    height: 104.w,
+                    decoration: ShapeDecoration(
+                      color: Colors.white.withValues(alpha: 0.12),
+                      shape: RoundedRectangleBorder(
+                        borderRadius: BorderRadius.circular(9.27.sp),
+                      ),
+                      image: DecorationImage(
+                        image: AssetEntityImageProvider(
+                          controller.photoGroup.value.images[index],
+                          thumbnailSize: const ThumbnailSize.square(300),
+                          isOriginal: false,
+                        ),
+                        fit: BoxFit.cover,
                       ),
-                      fit: BoxFit.cover,
                     ),
                   ),
-                ),
-                Positioned(
-                  right: 8.w,
-                  bottom: 8.h,
-                  child: Visibility(
-                      visible: controller.isSelectMode.value,
-                      child: GestureDetector(
-                          onTap: () =>
-                              controller.toggleImageSelection(images, index),
-                          child: Container(
-                            child: isSelected
-                                ? Center(
-                                    child: Assets.images.iconSelected.image(
-                                      width: 20.w,
-                                      height: 20.h,
-                                    ),
-                                  )
-                                : Center(
-                                    child: Assets.images.iconUnselected.image(
+                  Positioned(
+                    right: 8.w,
+                    bottom: 8.h,
+                    child: Visibility(
+                        visible: controller.isSelectMode.value,
+                        child: GestureDetector(
+                            onTap: () => controller.toggleImageSelection(index),
+                            child: Container(
+                              child: isSelected
+                                  ? Center(
+                                child: Assets.images.iconSelected.image(
+                                  width: 20.w,
+                                  height: 20.h,
+                                ),
+                              )
+                                  : Center(
+                                  child: Assets.images.iconUnselected.image(
                                     width: 20.w,
                                     height: 20.h,
                                   )),
-                          ))),
-                ),
-              ],
-            );
-          }),
-        );
+                            ))),
+                  ),
+                ],
+              ));
+        });
       };
 
   Widget _bottomBarCard() {
     return GestureDetector(
         onTap: () {
-          // controller.clickDelete();
+          controller.clickDelete();
         },
         child: Container(
           width: 328.w,
@@ -282,7 +286,8 @@ class CalendarMonthPage extends BasePage<CalendarMonthController> {
               SizedBox(width: 5.w),
               Obx(() {
                 return Text(
-                  'delete ${controller.selectedFilesSizeString}',
+                  'delete ${controller
+                      .selectedFilesSizeString}',
                   textAlign: TextAlign.center,
                   style: TextStyle(
                     color: Colors.white,

+ 16 - 1
lib/module/calendar/calendar_state.dart

@@ -1,8 +1,13 @@
 import 'package:intl/intl.dart';
+import 'package:get/get.dart';
+
+import '../people_photo/photo_group.dart';
 
 class CalendarState {
+  static final RxList<PhotoGroup> monthlyAlbums = <PhotoGroup>[].obs;
 
- static String formatMonth(String month) {
+  // 格式化月份
+  static String formatMonth(String month) {
     try {
       DateTime date = DateFormat('yyyy-MM').parse(month);
       return DateFormat('MMM , yyyy').format(date); // 转换为 "Nov, 2024" 格式
@@ -11,5 +16,15 @@ class CalendarState {
     }
   }
 
+  static void removePhotosData(Set<String> selectedPhotosIds) {
+    monthlyAlbums.removeWhere((album) {
+      album.images
+          .removeWhere((element) => selectedPhotosIds.contains(element.id));
+      album.selectedPhotosIds
+          .removeWhere((element) => selectedPhotosIds.contains(element));
 
+      // 如果images为空,删除该 album
+      return album.images.isEmpty;
+    });
+  }
 }

+ 53 - 2
lib/module/calendar/calendar_view.dart

@@ -28,6 +28,9 @@ class CalendarPage extends BaseView<CalendarController> {
             slivers: [
               SliverToBoxAdapter(child: titleCard()),
               Obx(() {
+                if (controller.monthlyAlbums.isEmpty) {
+                  return SliverToBoxAdapter(child: _noNoPicturesCard());
+                }
                 return SliverList(
                   delegate: SliverChildBuilderDelegate(
                         (context, index) {
@@ -65,7 +68,9 @@ class CalendarPage extends BaseView<CalendarController> {
             ),
           ),
           Spacer(),
-          Assets.images.iconCalendarSort.image(width: 28.w, height: 28.w),
+         GestureDetector(
+            onTap: () => controller.clickSort(),
+            child: Assets.images.iconCalendarSort.image(width: 28.w, height: 28.w)),
         ],
       ),
     );
@@ -122,7 +127,7 @@ class CalendarPage extends BaseView<CalendarController> {
                 children: List.generate(2, (index) {
                   if (index < photoGroup.images.length) {
                     return GestureDetector(
-                        onTap: controller.clickImage,
+                        onTap: () => controller.clickImage(photoGroup, index),
                         child: Container(
                           width: 146.w,
                           height: 146.w,
@@ -152,4 +157,50 @@ class CalendarPage extends BaseView<CalendarController> {
           ),
         ));
   }
+
+  Widget _noNoPicturesCard() {
+    return Column(
+      children: [
+
+        SizedBox(
+          height: 170.h,
+        ),
+        Container(
+          child: Column(
+            mainAxisAlignment: MainAxisAlignment.center,
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              Container(
+                width: 70.w,
+                height: 70.h,
+                clipBehavior: Clip.antiAlias,
+                decoration: BoxDecoration(),
+                child: Assets.images.iconNoPictures.image(),
+              ),
+              SizedBox(height: 22.h),
+              Text(
+                'No pictures found',
+                textAlign: TextAlign.center,
+                style: TextStyle(
+                  color: Colors.white,
+                  fontSize: 20.sp,
+                  fontWeight: FontWeight.w700,
+                ),
+              ),
+              SizedBox(height: 12.h),
+              Text(
+                'No pictures available at the moment',
+                textAlign: TextAlign.center,
+                style: TextStyle(
+                  color: Colors.white.withValues(alpha: 0.6),
+                  fontSize: 14.sp,
+                  fontWeight: FontWeight.w400,
+                ),
+              ),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
 }

+ 328 - 0
lib/module/calendar/preview/calendar_preview_controller.dart

@@ -0,0 +1,328 @@
+import 'dart:async';
+
+import 'package:clean/base/base_controller.dart';
+import 'package:clean/module/calendar/selected_preview/calendar_selected_preview_view.dart';
+import 'package:flutter/Material.dart';
+import 'package:flutter_card_swiper/flutter_card_swiper.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
+import 'package:wechat_assets_picker/wechat_assets_picker.dart';
+
+import '../../../data/repositories/user_repository.dart';
+import '../../../dialog/photo_delete_finish_dialog.dart';
+import '../../../dialog/photo_deleting_dialog.dart';
+import '../../../utils/toast_util.dart';
+import '../../image_picker/image_picker_util.dart';
+import '../../people_photo/photo_group.dart';
+import '../../store/store_view.dart';
+
+class CalendarPreviewController extends BaseController
+    with GetSingleTickerProviderStateMixin {
+  final Rx<PhotoGroup> photoGroup =
+      PhotoGroup(isSelected: false, images: []).obs;
+
+  late String? currentImageId;
+
+  final CardSwiperController cardSwiperController = CardSwiperController();
+
+  final RxBool isSwiperEnd = false.obs;
+  RxInt groupIndex = 0.obs;
+
+
+  late AnimationController animationController;
+  RxBool animationIsComplete = false.obs;
+
+  @override
+  void onInit() {
+    super.onInit();
+    _getArgs(); // 获取传递的参数
+    animationController = AnimationController(
+      vsync: this,
+      duration: const Duration(seconds: 3),
+    );
+
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      if (currentImageId != null) {
+        for (int i = 0; i < photoGroup.value.images.length; i++) {
+          if (photoGroup.value.images[i].id == currentImageId) {
+            debugPrint('photoGroups[i].id ${photoGroup.value.images[i].id},i $i');
+            groupIndex.value = i;
+            cardSwiperController.moveTo(i);
+            break;
+          }
+        }
+      }
+    });
+
+    animationController.addStatusListener((status) {
+      if (status == AnimationStatus.forward) {
+        //   延迟一秒
+        Future.delayed(Duration(seconds: 1), () {
+          animationIsComplete.value = true;
+        });
+      }
+      if (status == AnimationStatus.completed) {
+        Future.delayed(Duration(seconds: 1), () {
+          CalendarSelectedPreviewPage.start(photoGroup.value);
+        });
+      }
+    });
+  }
+
+  // 获取参数
+  void _getArgs() {
+    photoGroup.value = parameters?['photoGroup'];
+    currentImageId = parameters?['currentImageId'];
+  }
+
+  void clickUnselect() {
+    debugPrint("clickUnselect");
+    cardSwiperController.swipe(CardSwiperDirection.right);
+  }
+
+  FutureOr<bool> onSwipe(
+    int previousIndex,
+    int? currentIndex,
+    CardSwiperDirection direction,
+  ) {
+    debugPrint(
+      'The card $previousIndex was swiped to the ${direction.name}. Now the card $currentIndex is on top',
+    );
+
+    if (currentIndex != null) {
+      groupIndex.value = currentIndex;
+    }
+    // 预加载
+
+    // precacheImage(AssetEntityImageProvider(entity), context);
+
+    // 如果direction是left,
+    if (direction == CardSwiperDirection.left) {
+      //   先看看图片id是不是在selectedPhotosIds里面,如果在,不处理,如果不在,添加到selectedPhotosIds里面
+      if (!photoGroup.value.selectedPhotosIds
+          .contains(photoGroup.value.images[previousIndex].id)) {
+        debugPrint(
+            'add photoGroups[groupIndex.value].id ${photoGroup.value.images[previousIndex].id}');
+        photoGroup.value.selectedPhotosIds
+            .add(photoGroup.value.images[previousIndex].id);
+      }
+    } else if (direction == CardSwiperDirection.right) {
+      //   先看看图片id是不是在selectedPhotosIds里面,如果在,在selectedPhotosIds移除,不处理,如果不在,不处理
+      if (photoGroup.value.selectedPhotosIds
+          .contains(photoGroup.value.images[previousIndex].id)) {
+        debugPrint(
+            'remove photoGroups[groupIndex.value].id ${photoGroup.value.images[previousIndex].id}');
+        photoGroup.value.selectedPhotosIds
+            .remove(photoGroup.value.images[previousIndex].id);
+      }
+    }
+
+    updateSelectedFilesSize();
+    return true;
+  }
+
+  bool onSwiperUndo(
+    int? previousIndex,
+    int currentIndex,
+    CardSwiperDirection direction,
+  ) {
+    debugPrint(
+        'The card $currentIndex was swiped back to the ${direction.name}. Now the card $previousIndex is on top');
+
+    groupIndex.value = currentIndex;
+    //   撤销之前左滑的操作
+    if (direction == CardSwiperDirection.left) {
+      debugPrint(
+          'photoGroups[groupIndex.value].id ${photoGroup.value.images[groupIndex.value].id}');
+      if (photoGroup.value.selectedPhotosIds
+          .contains(photoGroup.value.images[groupIndex.value].id)) {
+        photoGroup.value.selectedPhotosIds
+            .remove(photoGroup.value.images[groupIndex.value].id);
+      }
+    }
+
+    updateSelectedFilesSize();
+
+    return true;
+  }
+
+  onSwiperEnd() {
+    isSwiperEnd.value = true;
+    debugPrint('onSwiperEnd');
+
+    // 延迟500ms
+    Future.delayed(Duration(milliseconds: 200), () {
+      animationController.forward(from: 0);
+    });
+    PhotoManager.clearFileCache();
+    // PhotoSelectedPreviewPage.start(photosType, selectedPhotosIds);
+  }
+
+  void recoverSelectPhoto() {
+    debugPrint('PhotoPreviewController recoverSelectPhoto');
+
+    cardSwiperController.undo();
+  }
+
+  void clickSelect() {
+    cardSwiperController.swipe(CardSwiperDirection.left);
+  }
+
+  void clickBack() {
+    Get.back();
+  }
+
+  clickDelete() async {
+    debugPrint('CalendarPreviewController clickDelete');
+    if (userRepository.isVip()) {
+
+      if (photoGroup.value.selectedPhotosIds.isNotEmpty) {
+        photoDeletingDialog();
+        // 获取要删除的资产
+        final assetsToDelete = photoGroup.value.images
+            .where(
+              (asset) => photoGroup.value.selectedPhotosIds.contains(asset.id),
+        )
+            .toList();
+
+        // 调用方法会返回被删除的资源,如果全部失败会返回空列表。
+        final List<String> result = await PhotoManager.editor.deleteWithIds(
+          assetsToDelete.map((e) => e.id).toList(),
+        );
+
+
+        //   比较result和selectedPhotosIds,如果result和selectedPhotosIds相等,说明删除成功,走下面的逻辑
+        // 如果不相等,说明有删除失败的,走else逻辑
+        if (result.length == photoGroup.value.selectedPhotosIds.length) {
+
+          ImagePickerUtil.updatePhotoData(
+              photoGroup.value.selectedPhotosIds);
+          cleanSelections();
+
+          ToastUtil.show('Delete success');
+          Future.delayed(Duration(seconds: 2), () {
+            SmartDialog.dismiss(tag: 'photoDeletingDialog');
+            photoDeleteFinishDialog();
+          });
+        } else {
+          SmartDialog.dismiss(tag: 'photoDeletingDialog');
+          // 删除失败
+          ToastUtil.show("Delete failed");
+        }
+      }
+
+    } else {
+      StorePage.start();
+    }
+    }
+
+  //删除成功清除选中的图片
+  void cleanSelections() async {
+    photoGroup.value.images
+        .removeWhere((element) => photoGroup.value.selectedPhotosIds.contains(element.id));
+    photoGroup.value.selectedPhotosIds.clear();
+    photoGroup.refresh();
+    if (photoGroup.value.images.isEmpty) {
+      return;
+    }
+
+    isSwiperEnd.value = false;
+
+    if (photoGroup.value.images.isNotEmpty) {
+      WidgetsBinding.instance.addPostFrameCallback((_) {
+        groupIndex.value = 0;
+        cardSwiperController.moveTo(0);
+      });
+    }
+    updateSelectedFilesSize();
+
+  }
+
+  // 将selectedFilesSize转成String类型,然后单位转换,如果超过1MB,则转成MB,超过1GB,则转成GB,否则KB
+  String get selectedFilesSizeString {
+
+    final double sizeInKB = photoGroup.value.selectedTotalSize.value;
+
+    if (sizeInKB >= 1024 * 1024) {  // 先检查最大单位(GB)
+      return "${(sizeInKB / (1024 * 1024)).toStringAsFixed(2)}GB";
+    } else if (sizeInKB >= 1024) {  // 然后检查MB
+      return "${(sizeInKB / 1024).toStringAsFixed(2)}MB";
+    } else {  // 最后是KB
+      return "${sizeInKB.toStringAsFixed(2)}KB";
+    }
+  }
+
+  Future<void> updateSelectedFilesSize() async {
+    // 如果没有选中的文件,直接返回
+    if (photoGroup.value.selectedCount == 0) {
+      photoGroup.value.selectedTotalSize.value = 0;
+      return;
+    }
+
+    double totalSize = 0;
+    final selectedImages = photoGroup.value.images;
+
+    // 获取选中图片的大小(异步并行执行)
+    final futures = photoGroup.value.selectedPhotosIds.map((assetId) async {
+      if (ImagePickerUtil.fileSizeCache.containsKey(assetId)) {
+        totalSize += ImagePickerUtil.fileSizeCache[assetId]!;
+      } else {
+        final image = selectedImages.firstWhere((image) => image.id == assetId);
+        final file = await image.file;
+        if (file != null) {
+          final size = (await file.length()) / 1024; // 转换为KB
+          totalSize += size;
+          ImagePickerUtil.fileSizeCache[assetId] = size;
+        }
+      }
+    }).toList();
+
+    // 等待所有异步操作完成
+    await Future.wait(futures);
+
+    // 更新选中文件的总大小
+    photoGroup.value.selectedTotalSize.value = totalSize;
+
+    // 打印调试信息
+    print("selectedFilesSize: ${photoGroup.value.selectedTotalSize.value}");
+
+
+  }
+
+  void restoreSelections() {
+
+
+    if(photoGroup.value.images.isEmpty){
+      clickBack();
+      return;
+    }
+
+    isSwiperEnd.value = false;
+
+    if (photoGroup.value.images.isNotEmpty) {
+      WidgetsBinding.instance.addPostFrameCallback((_) {
+        groupIndex.value = 0;
+        cardSwiperController.moveTo(0);
+      });
+    }
+    updateSelectedFilesSize();
+
+  }
+  @override
+  void onClose() {
+    debugPrint('CalendarPreviewController onClose');
+
+    animationController.dispose();
+    super.onClose();
+
+    isSwiperEnd.value = false;
+
+    // 清理操作,释放资源
+    cardSwiperController.dispose();
+
+    // 清理缓存
+    PhotoManager.clearFileCache();
+  }
+
+
+}

+ 402 - 0
lib/module/calendar/preview/calendar_preview_view.dart

@@ -0,0 +1,402 @@
+import 'package:clean/base/base_page.dart';
+import 'package:clean/module/calendar/preview/calendar_preview_controller.dart';
+import 'package:flutter/Material.dart';
+import 'package:flutter/src/widgets/framework.dart';
+import 'package:flutter_card_swiper/flutter_card_swiper.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import 'package:lottie/lottie.dart';
+import 'package:wechat_assets_picker/wechat_assets_picker.dart';
+
+import '../../../resource/assets.gen.dart';
+import '../../../router/app_pages.dart';
+import '../../people_photo/photo_group.dart';
+
+class CalendarPreviewPage extends BasePage<CalendarPreviewController> {
+  const CalendarPreviewPage({Key? key}) : super(key: key);
+
+  static void start(
+      {required PhotoGroup photoGroup, required String currentImageId}) {
+    Get.toNamed(RoutePath.calendarPreview, arguments: {
+      "photoGroup": photoGroup,
+      "currentImageId": currentImageId,
+    });
+  }
+
+  @override
+  bool statusBarDarkFont() {
+    return false;
+  }
+
+  @override
+  bool immersive() {
+    return true;
+  }
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Stack(children: [
+      PopScope(
+        canPop: false,
+        onPopInvokedWithResult: (didPop, result) {
+          if (didPop) {
+            return;
+          }
+          controller.clickBack();
+        },
+        child: SafeArea(
+          child: Obx(() {
+            if (controller.isSwiperEnd.value ||
+                controller.photoGroup.value.images.isEmpty) {
+              return onSwiperEndCard();
+            } else {
+              return Column(
+                children: [
+                  _titleCard(),
+
+                  Spacer(),
+                  SizedBox(
+                    width: 314.w,
+                    height: 392.h,
+                    child: CardSwiper(
+                      scale: 0.8,
+                      allowedSwipeDirection: AllowedSwipeDirection.only(
+                        right: true,
+                        left: true,
+                      ),
+                      isLoop: false,
+                      backCardOffset: Offset(0.w, -20.h),
+                      controller: controller.cardSwiperController,
+                      cardsCount: controller.photoGroup.value.images.length,
+                      onSwipe: controller.onSwipe,
+                      onUndo: controller.onSwiperUndo,
+                      numberOfCardsDisplayed:
+                          (controller.photoGroup.value.images.length == 1)
+                              ? 1
+                              : 2,
+                      onEnd: controller.onSwiperEnd,
+                      cardBuilder: (context, index, horizontalOffsetPercentage,
+                          verticalOffsetPercentage) {
+
+                        final assetEntity =
+                            controller.photoGroup.value.images[index];
+                        return Stack(
+                          children: [
+
+                            ClipRRect(
+                                borderRadius: BorderRadius.circular(20.r),
+                                child:
+                                AssetEntityImage(
+                                  assetEntity,
+                                  width: 314.w,
+                                  height: 392.h,
+                                  fit: BoxFit.cover,
+                                  isOriginal: true,
+                                  errorBuilder: (context, error, stackTrace) {
+                                    debugPrint('AssetEntityImage error $error');
+                                    return Container(
+                                    );
+                                  },
+                                )),
+                            if (horizontalOffsetPercentage != 0)
+                              Positioned(
+                                top: 0,
+                                right: horizontalOffsetPercentage < -10.w
+                                    ? 0
+                                    : null,
+                                left: horizontalOffsetPercentage > 10.w
+                                    ? 0
+                                    : null,
+                                child: Container(
+                                    child: Column(
+                                  children: [
+                                    (horizontalOffsetPercentage < -10.w)
+                                        ? Assets.images.iconPhotoPreviewDelete
+                                            .image(
+                                            width: 60.w,
+                                            height: 60.w,
+                                          )
+                                        : (horizontalOffsetPercentage > 10.w)
+                                            ? Assets.images.iconPhotoPreviewKeep
+                                                .image(
+                                                width: 60.w,
+                                                height: 60.w,
+                                              )
+                                            : SizedBox(),
+                                    (horizontalOffsetPercentage < -10.w)
+                                        ? Text(
+                                            'Delete ',
+                                            style: TextStyle(
+                                              color: Colors.white,
+                                              fontSize: 24.sp,
+                                              fontWeight: FontWeight.w500,
+                                            ),
+                                          )
+                                        : (horizontalOffsetPercentage > 10.w)
+                                            ? Text(
+                                                'Keep',
+                                                style: TextStyle(
+                                                  color: Colors.white,
+                                                  fontSize: 24.sp,
+                                                  fontWeight: FontWeight.w500,
+                                                ),
+                                              )
+                                            : SizedBox(),
+                                  ],
+                                )),
+                              ),
+                          ],
+                        );
+                      },
+                    ),
+                  ),
+                  Spacer(),
+                  bottomButtonCard(),
+                  _bottomBarCard(),
+                ],
+              );
+            }
+          }),
+        ),
+      ),
+      Obx(() {
+        if (controller.isSwiperEnd.value ||
+            controller.photoGroup.value.images.isEmpty) {
+          return IgnorePointer(
+            child: Assets.images.bgPhotoSelectedPreviewFinish.image(
+              width: 360.w,
+            ),
+          );
+        } else {
+          return IgnorePointer(
+            child: Assets.images.bgHome.image(
+              width: 360.w,
+            ),
+          );
+        }
+      }),
+    ]);
+  }
+
+  Widget _titleCard() {
+    return Container(
+      alignment: Alignment.centerLeft,
+      padding: EdgeInsets.only(left: 16.w, top: 14.h, right: 16.w),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Row(
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            children: [
+              GestureDetector(
+                onTap: controller.clickBack,
+                child: Assets.images.iconBackArrow.image(
+                  width: 28.w,
+                  height: 28.h,
+                ),
+              ),
+              Obx(() => Row(
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    children: [
+                      Text(
+                        '${controller.groupIndex.value + 1}',
+                        textAlign: TextAlign.center,
+                        style: TextStyle(
+                          color: Colors.white,
+                          fontSize: 16.sp,
+                          fontWeight: FontWeight.w700,
+                        ),
+                      ),
+                      Text(
+                        ' / ${controller.photoGroup.value.images.length}',
+                        textAlign: TextAlign.center,
+                        style: TextStyle(
+                          color: Colors.white.withValues(alpha: 0.6),
+                          fontSize: 16.sp,
+                          fontWeight: FontWeight.w500,
+                        ),
+                      ),
+                    ],
+                  )),
+              GestureDetector(
+                onTap: controller.recoverSelectPhoto,
+                child: Assets.images.iconPreviewRecover.image(
+                  width: 30.w,
+                  height: 30.h,
+                ),
+              ),
+            ],
+          ),
+          SizedBox(height: 12.h),
+        ],
+      ),
+    );
+  }
+
+  Widget _bottomBarCard() {
+    return Container(
+      width: 360.w,
+      height: 81.h,
+      padding: EdgeInsets.symmetric(horizontal: 16.w),
+      decoration: ShapeDecoration(
+        color: Color(0xFF23232A),
+        shape: RoundedRectangleBorder(
+          side: BorderSide(
+              width: 1.w, color: Colors.white.withValues(alpha: 0.1)),
+          borderRadius: BorderRadius.only(
+            topLeft: Radius.circular(14.r),
+            topRight: Radius.circular(14.r),
+          ),
+        ),
+      ),
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: [
+          Obx(() {
+            return Text(
+              '${controller.photoGroup.value.selectedPhotosIds.length} files selected (${controller.selectedFilesSizeString} )',
+              textAlign: TextAlign.center,
+              style: TextStyle(
+                color: Colors.white.withValues(alpha: 0.9),
+                fontSize: 13.sp,
+                fontWeight: FontWeight.w500,
+              ),
+            );
+          }),
+          GestureDetector(
+              onTap: controller.clickDelete,
+              child: Container(
+                width: 108.w,
+                height: 38.h,
+                decoration: ShapeDecoration(
+                  color: Color(0xFF0279FB),
+                  shape: RoundedRectangleBorder(
+                    borderRadius: BorderRadius.circular(10.r),
+                  ),
+                ),
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+                  children: [
+                    Text(
+                      'Delete',
+                      textAlign: TextAlign.center,
+                      style: TextStyle(
+                        color: Colors.white,
+                        fontSize: 16.sp,
+                        fontWeight: FontWeight.w500,
+                      ),
+                    ),
+                    Assets.images.iconDelete.image(
+                      width: 18.w,
+                      height: 18.h,
+                    ),
+                  ],
+                ),
+              )),
+        ],
+      ),
+    );
+  }
+
+  Widget bottomButtonCard() {
+    return Container(
+      margin: EdgeInsets.only(bottom: 54.h),
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+        children: [
+          GestureDetector(
+            onTap: () => controller.clickSelect(),
+            child: Assets.images.iconPreviewSelect.image(
+              width: 62.w,
+              height: 62.h,
+            ),
+          ),
+          GestureDetector(
+            onTap: () => controller.clickUnselect(),
+            child: Assets.images.iconPreviewNoSelect.image(
+              width: 62.w,
+              height: 62.h,
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  Widget onSwiperEndCard() {
+    return Column(
+      children: [
+        Container(
+          child: Lottie.asset(
+            Assets.anim.animFireworks,
+            controller: controller.animationController,
+            height: 351.h,
+            repeat: false,
+          ),
+        ),
+        Visibility(
+            visible: controller.animationIsComplete.value,
+            child: Center(
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.center,
+                mainAxisAlignment: MainAxisAlignment.center,
+                children: [
+                  Text(
+                    'Perfect!',
+                    textAlign: TextAlign.center,
+                    style: TextStyle(
+                      color: Colors.white,
+                      fontSize: 32.sp,
+                      fontWeight: FontWeight.w700,
+                    ),
+                  ),
+                  SizedBox(height: 16.h),
+                  SizedBox(
+                    child: Row(
+                      mainAxisAlignment: MainAxisAlignment.center,
+                      children: [
+                        Container(
+                          clipBehavior: Clip.antiAlias,
+                          decoration: BoxDecoration(),
+                          child:
+                              Assets.images.iconPreviewSwiperEndFirework.image(
+                            width: 40.w,
+                            height: 40.w,
+                          ),
+                        ),
+                        SizedBox(width: 4.w),
+                        Text(
+                          'All Similar and Redundant\nPhotos Cleared',
+                          textAlign: TextAlign.center,
+                          style: TextStyle(
+                            color: Colors.white.withValues(alpha: 0.9),
+                            fontSize: 16.sp,
+                            fontWeight: FontWeight.w400,
+                          ),
+                        ),
+                        SizedBox(width: 4.w),
+                        Container(
+                          clipBehavior: Clip.antiAlias,
+                          decoration: BoxDecoration(),
+                          child:
+                              Assets.images.iconPreviewSwiperEndFirework.image(
+                            width: 40.w,
+                            height: 40.w,
+                          ),
+                        ),
+                      ],
+                    ),
+                  ),
+                ],
+              ),
+            )),
+        Spacer(
+          flex: 5,
+        ),
+        controller.photoGroup.value.images.isEmpty
+            ? SizedBox()
+            : _bottomBarCard(),
+      ],
+    );
+  }
+}

+ 211 - 0
lib/module/calendar/selected_preview/calendar_selected_preview_controller.dart

@@ -0,0 +1,211 @@
+import 'package:clean/base/base_controller.dart';
+import 'package:clean/module/calendar/preview/calendar_preview_controller.dart';
+import 'package:flutter/Material.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
+import 'package:wechat_assets_picker/wechat_assets_picker.dart';
+
+import '../../../data/repositories/user_repository.dart';
+import '../../../dialog/photo_delete_finish_dialog.dart';
+import '../../../dialog/photo_deleting_dialog.dart';
+import '../../../router/app_pages.dart';
+import '../../../utils/toast_util.dart';
+import '../../image_picker/image_picker_util.dart';
+import '../../people_photo/photo_group.dart';
+import '../../store/store_view.dart';
+
+class CalendarSelectedPreviewController extends BaseController {
+  final Rx<PhotoGroup> photoGroup =
+      PhotoGroup(isSelected: false, images: []).obs;
+
+  final CalendarPreviewController calendarPreviewController =
+      Get.find<CalendarPreviewController>();
+
+  final RxBool isKeepAll = false.obs;
+
+  @override
+  void onInit() {
+    super.onInit();
+    _initData();
+  }
+
+  void _initData() {
+    // 筛选出已选中的图片
+    final selectedImages = calendarPreviewController.photoGroup.value.images
+        .where((image) => calendarPreviewController
+            .photoGroup.value.selectedPhotosIds
+            .contains(image.id))
+        .toList();
+
+    // 生成新的 PhotoGroup 仅包含选中的图片
+    photoGroup.value = PhotoGroup(
+      isSelected: selectedImages.length ==
+          calendarPreviewController.photoGroup.value.images.length,
+      images: selectedImages,
+    );
+
+    // 更新已选中的 ID 列表
+    photoGroup.value.selectedPhotosIds.addAll(
+      selectedImages.map((image) => image.id),
+    );
+    updateSelectedFilesSize();
+  }
+
+  void clickBack() {
+    calendarPreviewController.restoreSelections();
+    Get.back();
+  }
+
+  void toggleGroupSelection(List<AssetEntity> imagesList) {
+    final newValue = !photoGroup.value.isSelected.value;
+    photoGroup.value.toggleSelectAll(newValue);
+
+    // 更新选中状态
+    for (var image in imagesList) {
+      updateSelectedPhotosIds(image.id, newValue);
+    }
+    updateSelectedFilesSize();
+  }
+
+  clickDelete() async {
+    debugPrint('CalendarSelectedPreviewController clickDelete');
+    if (userRepository.isVip()) {
+
+    if (photoGroup.value.selectedPhotosIds.isNotEmpty) {
+      photoDeletingDialog();
+      // 获取要删除的资产
+      final assetsToDelete = photoGroup.value.images
+          .where(
+            (asset) => photoGroup.value.selectedPhotosIds.contains(asset.id),
+          )
+          .toList();
+
+      // 调用方法会返回被删除的资源,如果全部失败会返回空列表。
+      final List<String> result = await PhotoManager.editor.deleteWithIds(
+        assetsToDelete.map((e) => e.id).toList(),
+      );
+
+      //   比较result和selectedPhotosIds,如果result和selectedPhotosIds相等,说明删除成功,走下面的逻辑
+      // 如果不相等,说明有删除失败的,走else逻辑
+      if (result.length == photoGroup.value.selectedPhotosIds.length) {
+        debugPrint(
+            "CalendarSelectedPreviewController delete ${photoGroup.value.selectedPhotosIds}");
+
+        ImagePickerUtil.updatePhotoData(photoGroup.value.selectedPhotosIds);
+        cleanSelections();
+        ToastUtil.show('Delete success');
+        Future.delayed(Duration(seconds: 2), () {
+          SmartDialog.dismiss(tag: 'photoDeletingDialog');
+          photoDeleteFinishDialog(onDismiss: () {
+            Get.until((route) => Get.currentRoute == RoutePath.mainTab);
+          });
+
+        });
+      } else {
+        SmartDialog.dismiss(tag: 'photoDeletingDialog');
+        // 删除失败
+        ToastUtil.show("Delete failed");
+      }
+    }
+    } else {
+      StorePage.start();
+    }
+  }
+
+  //删除成功清除选中的图片
+  void cleanSelections() async {
+    photoGroup.value.images.removeWhere(
+        (element) => photoGroup.value.selectedPhotosIds.contains(element.id));
+    calendarPreviewController.photoGroup.value.images.removeWhere(
+        (element) => photoGroup.value.selectedPhotosIds.contains(element.id));
+    calendarPreviewController.photoGroup.value.selectedPhotosIds.clear();
+    photoGroup.value.selectedPhotosIds.clear();
+    if (photoGroup.value.images.isEmpty) {
+      return;
+    }
+    updateSelectedFilesSize();
+  }
+
+  Future<void> updateSelectedFilesSize() async {
+    // 如果没有选中的文件,直接返回
+    if (photoGroup.value.selectedCount == 0) {
+      photoGroup.value.selectedTotalSize.value = 0;
+      return;
+    }
+
+    double totalSize = 0;
+    final selectedImages = photoGroup.value.images;
+
+    // 获取选中图片的大小(异步并行执行)
+    final futures = photoGroup.value.selectedPhotosIds.map((assetId) async {
+      if (ImagePickerUtil.fileSizeCache.containsKey(assetId)) {
+        totalSize += ImagePickerUtil.fileSizeCache[assetId]!;
+      } else {
+        final image = selectedImages.firstWhere((image) => image.id == assetId);
+        final file = await image.file;
+        if (file != null) {
+          final size = (await file.length()) / 1024; // 转换为KB
+          totalSize += size;
+          ImagePickerUtil.fileSizeCache[assetId] = size;
+        }
+      }
+    }).toList();
+
+    // 等待所有异步操作完成
+    await Future.wait(futures);
+
+    // 更新选中文件的总大小
+    photoGroup.value.selectedTotalSize.value = totalSize;
+
+    // 打印调试信息
+    debugPrint(
+        "selectedFilesSize: ${photoGroup.value.selectedTotalSize.value}");
+
+    // 清理缓存
+    PhotoManager.clearFileCache();
+  }
+
+  void updateSelectedPhotosIds(String photoId, bool isSelected) {
+    if (isSelected) {
+      if (!photoGroup.value.selectedPhotosIds.contains(photoId)) {
+        photoGroup.value.selectedPhotosIds.add(photoId);
+      }
+    } else {
+      photoGroup.value.selectedPhotosIds.remove(photoId);
+    }
+    photoGroup.value.isSelected.value =
+        photoGroup.value.selectedPhotosIds.length ==
+            photoGroup.value.images.length;
+  }
+
+  clickImage(int imageIndex) {
+    debugPrint("CalendarSelectedPreviewController clickImage");
+  }
+
+  Future<void> toggleImageSelection(int imageIndex) async {
+    debugPrint("CalendarSelectedPreviewController toggleImageSelection");
+    final image = photoGroup.value.images[imageIndex];
+    final photoId = image.id;
+
+    // 反转选择状态
+    final isNowSelected = !photoGroup.value.selectedPhotosIds.contains(photoId);
+    updateSelectedPhotosIds(photoId, isNowSelected);
+
+    updateSelectedFilesSize();
+  }
+
+  // 将selectedFilesSize转成String类型,然后单位转换,如果超过1MB,则转成MB,超过1GB,则转成GB,否则KB
+  String get selectedFilesSizeString {
+
+    final double sizeInKB = photoGroup.value.selectedTotalSize.value;
+
+    if (sizeInKB >= 1024 * 1024) {  // 先检查最大单位(GB)
+      return "${(sizeInKB / (1024 * 1024)).toStringAsFixed(2)}GB";
+    } else if (sizeInKB >= 1024) {  // 然后检查MB
+      return "${(sizeInKB / 1024).toStringAsFixed(2)}MB";
+    } else {  // 最后是KB
+      return "${sizeInKB.toStringAsFixed(2)}KB";
+    }
+  }
+
+}

+ 348 - 0
lib/module/calendar/selected_preview/calendar_selected_preview_view.dart

@@ -0,0 +1,348 @@
+import 'package:clean/base/base_controller.dart';
+import 'package:clean/base/base_page.dart';
+import 'package:clean/module/calendar/selected_preview/calendar_selected_preview_controller.dart';
+import 'package:flutter/Material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import 'package:lottie/lottie.dart';
+import 'package:wechat_assets_picker/wechat_assets_picker.dart';
+
+import '../../../resource/assets.gen.dart';
+import '../../../router/app_pages.dart';
+import '../../people_photo/photo_group.dart';
+
+class CalendarSelectedPreviewPage
+    extends BasePage<CalendarSelectedPreviewController> {
+  const CalendarSelectedPreviewPage({Key? key}) : super(key: key);
+
+  static void start(PhotoGroup photoGroup) {
+    Get.toNamed(RoutePath.calendarSelectedPreview,
+        arguments: {"photoGroup": photoGroup});
+  }
+
+  @override
+  bool statusBarDarkFont() {
+    return false;
+  }
+
+  @override
+  bool immersive() {
+    return true;
+  }
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Stack(children: [
+      PopScope(
+          canPop: false,
+          onPopInvokedWithResult: (didPop, result) {
+            if (didPop) {
+              return;
+            }
+            controller.clickBack();
+          },
+          child: Container(
+            child: SafeArea(
+              child: Obx(() {
+                if (controller.photoGroup.value.images.isEmpty) {
+                  return _noNoPicturesCard();
+                } else if (controller.isKeepAll.value) {
+                  return _finishCleanCard();
+                }
+                return Column(
+                  children: [
+                    _titleCard(),
+                    Flexible(
+                      child: Obx(() {
+                        return SizedBox(
+                          child: GridView.builder(
+                              padding: EdgeInsets.symmetric(horizontal: 16.w),
+                              scrollDirection: Axis.vertical,
+                              // 设置为垂直方向滚动
+                              physics: BouncingScrollPhysics(),
+                              gridDelegate:
+                                  SliverGridDelegateWithFixedCrossAxisCount(
+                                crossAxisCount: 3, // 每行显示 2 个元素
+                                mainAxisSpacing: 8.w, // 垂直间距
+                                crossAxisSpacing: 8.h, // 水平间距
+                              ),
+                              itemCount:
+                                  controller.photoGroup.value.images.length,
+                              itemBuilder: _buildPhotoItem(
+                                  controller.photoGroup.value.images)),
+                        );
+                      }),
+                    ),
+                    Obx(() {
+                      if (controller.photoGroup.value.images.isNotEmpty) {
+                        return _bottomBarCard();
+                      } else {
+                        return SizedBox();
+                      }
+                    }),
+                    SizedBox(height: 8.h),
+                  ],
+                );
+              }),
+            ),
+          )),
+      Obx(() {
+        if (controller.isKeepAll.value) {
+          return IgnorePointer(
+            child: Assets.images.bgPhotoSelectedPreviewFinish.image(
+              width: 360.w,
+            ),
+          );
+        } else {
+          return IgnorePointer(
+            child: Assets.images.bgHome.image(
+              width: 360.w,
+            ),
+          );
+        }
+      }),
+    ]);
+  }
+
+  Widget _titleCard() {
+    return Container(
+      alignment: Alignment.centerLeft,
+      padding:
+          EdgeInsets.only(left: 16.w, top: 14.h, right: 16.w, bottom: 20.h),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Obx(() {
+            return Row(
+              mainAxisAlignment: MainAxisAlignment.spaceBetween,
+              children: [
+                GestureDetector(
+                  onTap: () => controller.clickBack(),
+                  child: Assets.images.iconBackArrow.image(
+                    width: 28.w,
+                    height: 28.h,
+                  ),
+                ),
+                // 如果photoGroup数据为空,不显示全选按钮
+                controller.photoGroup.value.images.isEmpty
+                    ? Container()
+                    : GestureDetector(
+                        onTap: () => controller.toggleGroupSelection(
+                            controller.photoGroup.value.images),
+                        child: Obx(() => Text(
+                              controller.photoGroup.value.isSelected.value
+                                  ? 'Deselect All'
+                                  : 'Select All',
+                              style: TextStyle(
+                                color: Colors.white.withValues(alpha: 0.7),
+                                fontSize: 14.sp,
+                                fontWeight: FontWeight.w400,
+                              ),
+                            )),
+                      ),
+              ],
+            );
+          }),
+        ],
+      ),
+    );
+  }
+
+  Widget _bottomBarCard() {
+    return GestureDetector(
+        onTap: () {
+          controller.clickDelete();
+        },
+        child: Container(
+          width: 328.w,
+          height: 48.h,
+          decoration: ShapeDecoration(
+            color: Color(0xFF0279FB),
+            shape: RoundedRectangleBorder(
+              borderRadius: BorderRadius.circular(10.r),
+            ),
+          ),
+          padding: EdgeInsets.symmetric(horizontal: 16.w),
+          child: Row(
+            mainAxisAlignment: MainAxisAlignment.center,
+            children: [
+              Assets.images.iconDelete.image(
+                width: 18.w,
+                height: 18.h,
+              ),
+              SizedBox(width: 5.w),
+              Obx(() {
+                if (controller.photoGroup.value.selectedPhotosIds.isNotEmpty) {
+                  return Text(
+                    'delete ${controller.selectedFilesSizeString}',
+                    textAlign: TextAlign.center,
+                    style: TextStyle(
+                      color: Colors.white,
+                      fontSize: 16.sp,
+                      fontWeight: FontWeight.w500,
+                    ),
+                  );
+                } else {
+                  return Text(
+                    'Keep All',
+                    textAlign: TextAlign.center,
+                    style: TextStyle(
+                      color: Colors.white,
+                      fontSize: 16.sp,
+                      fontWeight: FontWeight.w500,
+                    ),
+                  );
+                }
+              }),
+            ],
+          ),
+        ));
+  }
+
+  Widget Function(BuildContext, int) _buildPhotoItem(
+          List<AssetEntity> images) =>
+      (context, index) {
+        return GestureDetector(
+          onTap: () => controller.clickImage(index),
+          child: Obx(() {
+            final photoId = controller.photoGroup.value.images[index].id;
+            final isSelected = controller.photoGroup.value.selectedPhotosIds.contains(photoId);
+            return Stack(
+              children: [
+                Container(
+                  width: 104.w,
+                  height: 104.w,
+                  decoration: ShapeDecoration(
+                    color: Colors.white.withValues(alpha: 0.12),
+                    shape: RoundedRectangleBorder(
+                      borderRadius: BorderRadius.circular(9.27.sp),
+                    ),
+                    image: DecorationImage(
+                      image: AssetEntityImageProvider(
+                        controller.photoGroup.value.images[index],
+                        thumbnailSize: const ThumbnailSize.square(300),
+                        isOriginal: false,
+                      ),
+                      fit: BoxFit.cover,
+                    ),
+                  ),
+                ),
+                Positioned(
+                  right: 8.w,
+                  bottom: 8.h,
+                  child: GestureDetector(
+                      onTap: () => controller.toggleImageSelection(index),
+                      child: Container(
+                        child: isSelected
+                            ? Center(
+                                child: Assets.images.iconSelected.image(
+                                  width: 20.w,
+                                  height: 20.h,
+                                ),
+                              )
+                            : Center(
+                                child: Assets.images.iconUnselected.image(
+                                width: 20.w,
+                                height: 20.h,
+                              )),
+                      )),
+                ),
+              ],
+            );
+          }),
+        );
+      };
+
+  Widget _noNoPicturesCard() {
+    return Column(
+      mainAxisAlignment: MainAxisAlignment.start,
+      children: [
+        _titleCard(),
+        SizedBox(height: 170.h),
+        Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          crossAxisAlignment: CrossAxisAlignment.center,
+          children: [
+            Container(
+              width: 70.w,
+              height: 70.h,
+              clipBehavior: Clip.antiAlias,
+              decoration: BoxDecoration(),
+              child: Assets.images.iconNoPictures.image(),
+            ),
+            SizedBox(height: 22.h),
+            Text(
+              'No pictures found',
+              textAlign: TextAlign.center,
+              style: TextStyle(
+                color: Colors.white,
+                fontSize: 20.sp,
+                fontWeight: FontWeight.w700,
+              ),
+            ),
+            SizedBox(height: 12.h),
+            Text(
+              'No pictures available at the moment',
+              textAlign: TextAlign.center,
+              style: TextStyle(
+                color: Colors.white.withValues(alpha: 0.6),
+                fontSize: 14.sp,
+                fontWeight: FontWeight.w400,
+              ),
+            ),
+          ],
+        ),
+      ],
+    );
+  }
+
+  Widget _finishCleanCard() {
+    return Container(
+      width: 360.w,
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.start,
+        children: [
+          Spacer(
+            flex: 3,
+          ),
+          Text(
+            'Great!',
+            textAlign: TextAlign.center,
+            style: TextStyle(
+              color: Colors.white,
+              fontSize: 32.sp,
+              fontWeight: FontWeight.w700,
+            ),
+          ),
+          SizedBox(height: 14.h),
+          Text(
+            "You've completed 1 photo review",
+            textAlign: TextAlign.center,
+            style: TextStyle(
+              color: Colors.white.withValues(alpha: 0.8),
+              fontSize: 16.sp,
+              fontWeight: FontWeight.w400,
+            ),
+          ),
+          SizedBox(height: 47.h),
+          // Assets.images.iconPhotoSelectedPreviewFireworks.image(
+          //   width: 158.w,
+          //   height: 158.h,
+          // ),
+
+          SizedBox(
+              child: Lottie.asset(
+            Assets.anim.animPhotoSelectedPreviewFireworks,
+            width: 158.w,
+            height: 158.w,
+            repeat: false,
+          )),
+
+          Spacer(
+            flex: 5,
+          ),
+        ],
+      ),
+    );
+  }
+}

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

@@ -94,7 +94,7 @@ class HomeController extends BaseController {
         ImagePickerUtil.locationPhotos.isEmpty ||
         ImagePickerUtil.screenshotPhotos.isEmpty) {
       try {
-        final List<AssetEntity> result = await ImagePickerUtil.loadAssetsAndroid();
+        final List<AssetEntity> result = await ImagePickerUtil.loadAssets();
         ImagePickerUtil.peoplePhotos.value = result ?? [];
 
         ImagePickerUtil.locationPhotos['location'] = result ?? [];

+ 66 - 37
lib/module/image_picker/image_picker_util.dart

@@ -10,9 +10,12 @@ import 'package:clean/module/people_photo/photo_group.dart';
 import 'package:clean/module/photo_preview/photo_preview_controller.dart';
 import 'package:clean/module/screenshots_blurry/screenshots_controller.dart';
 import 'package:clean/module/similar_photo/similar_photo_controller.dart';
+import 'package:flutter/widgets.dart';
 import 'package:get/get.dart';
 import 'package:photo_manager/photo_manager.dart';
 
+import '../calendar/calendar_state.dart';
+
 class ImagePickerUtil {
   ImagePickerUtil._();
 
@@ -38,6 +41,10 @@ class ImagePickerUtil {
   // 人物图片
   static final RxList<AssetEntity> peoplePhotos = <AssetEntity>[].obs;
 
+  // android测试
+
+  static late List<AssetEntity>  androidPhotos = <AssetEntity>[];
+
   static final RxSet<String> selectedScreenshotPhotosIds = <String>{}.obs;
   static final RxSet<String> selectedSimilarPhotosIds = <String>{}.obs;
   static final RxSet<String> selectedLocationPhotosIds = <String>{}.obs;
@@ -64,54 +71,36 @@ class ImagePickerUtil {
   }
 
 
-  static Future<void> updatePhotoGroupDate(PhotosType photosType,Set<String> selectedPhotosIds) async {
-    // 1. 统一移除相关的照片列表
-    screenshotPhotos.removeWhere((element) => selectedPhotosIds.contains(element.id));
-    for (var group in similarPhotos) {
-      group.removeWhere((element) => selectedPhotosIds.contains(element.id));
-    }
-    locationPhotos.forEach((key, group) => group.removeWhere((element) => selectedPhotosIds.contains(element.id)));
-    peoplePhotos.removeWhere((element) => selectedPhotosIds.contains(element.id));
-    blurryPhotos.removeWhere((element) => selectedPhotosIds.contains(element.id));
-    // 2. 移除空的集合
-    similarPhotos.removeWhere((element) => element.isEmpty);
-    locationPhotos.removeWhere((key, value) => value.isEmpty);
-
-
-    selectedScreenshotPhotosIds.removeWhere((element) => selectedPhotosIds.contains(element));
-    selectedSimilarPhotosIds.removeWhere((element) => selectedPhotosIds.contains(element));
-    selectedLocationPhotosIds.removeWhere((element) => selectedPhotosIds.contains(element));
-    selectedPeoplePhotosIds.removeWhere((element) => selectedPhotosIds.contains(element));
-    selectedBlurryPhotosIds.removeWhere((element) => selectedPhotosIds.contains(element));
-
 
+  static Future<void> updatePhotoGroupDate(PhotosType photosType,Set<String> selectedPhotosIds1) async {
+    // 1. 统一移除相关的照片列表
+    debugPrint("updatePhotoGroupDate1 ScreenshotsController screenshotController $selectedPhotosIds1");
+    // 2. 移除控制器中的数据
+    Set<String> selectedPhotosIds = Set.from(selectedPhotosIds1);
+    updatePhotoData(selectedPhotosIds);
 
+    debugPrint("updatePhotoGroupDate2 ScreenshotsController screenshotController $selectedPhotosIds");
     // 3. 根据photosType来处理不同的控制器和photoGroups
     switch (photosType) {
       case PhotosType.screenshots:
         ScreenShotsController screenshotController = Get.find<ScreenShotsController>();
-        screenshotController.photoGroups.removeWhere((element) => element.images.any((image) => selectedPhotosIds.contains(image.id)));
-        screenshotController.photoGroups.removeWhere((element) => element.images.isEmpty);
+        removeImagesAndEmptyGroups(screenshotController, selectedPhotosIds);
         selectedScreenshotPhotosIds.removeWhere((element) => selectedPhotosIds.contains(element));
-
         screenshotController.restoreSelections();
         break;
 
       case PhotosType.similarPhotos:
         SimilarPhotoController similarController = Get.find<SimilarPhotoController>();
-        similarController.photoGroups.removeWhere((element) => element.images.any((image) => selectedPhotosIds.contains(image.id)));
-        similarController.photoGroups.removeWhere((element) => element.images.isEmpty);
+        removeImagesAndEmptyGroups(similarController, selectedPhotosIds);
         selectedSimilarPhotosIds.removeWhere((element) => selectedPhotosIds.contains(element));
         similarController.restoreSelections();
         break;
 
       case PhotosType.locationPhotos:
         LocationsSinglePhotoController locationsSingleController = Get.find<LocationsSinglePhotoController>();
-        locationsSingleController.photoGroups.removeWhere((element) => element.images.any((image) => selectedPhotosIds.contains(image.id)));
-        locationsSingleController.photoGroups.removeWhere((element) => element.images.isEmpty);
         LocationsPhotoController locationsPhotoController = Get.find<LocationsPhotoController>();
-        locationsPhotoController.photoGroups.removeWhere((element) => element.images.any((image) => selectedPhotosIds.contains(image.id)));
-        locationsPhotoController.photoGroups.removeWhere((element) => element.images.isEmpty);
+        removeImagesAndEmptyGroups(locationsSingleController, selectedPhotosIds);
+        removeImagesAndEmptyGroups(locationsPhotoController, selectedPhotosIds);
         selectedLocationPhotosIds.removeWhere((element) => selectedPhotosIds.contains(element));
         locationsSingleController.restoreSelections();
         locationsPhotoController.restoreSelections();
@@ -119,21 +108,55 @@ class ImagePickerUtil {
 
       case PhotosType.peoplePhotos:
         PeoplePhotoController peoplePhotoController = Get.find<PeoplePhotoController>();
-        peoplePhotoController.photoGroups.removeWhere((element) => element.images.any((image) => selectedPhotosIds.contains(image.id)));
-        peoplePhotoController.photoGroups.removeWhere((element) => element.images.isEmpty);
+        removeImagesAndEmptyGroups(peoplePhotoController, selectedPhotosIds);
         selectedPeoplePhotosIds.removeWhere((element) => selectedPhotosIds.contains(element));
         peoplePhotoController.restoreSelections();
         break;
 
       case PhotosType.blurryPhotos:
         ScreenShotsController blurryController = Get.find<ScreenShotsController>();
-        blurryController.photoGroups.removeWhere((element) => element.images.any((image) => selectedPhotosIds.contains(image.id)));
-        blurryController.photoGroups.removeWhere((element) => element.images.isEmpty);
+        removeImagesAndEmptyGroups(blurryController, selectedPhotosIds);
         selectedBlurryPhotosIds.removeWhere((element) => selectedPhotosIds.contains(element));
         blurryController.restoreSelections();
         break;
+    }
+  }
+  // 移除每组图片里面对应的元素并删除空的组
+  static void removeImagesAndEmptyGroups(dynamic controller, Set<String> selectedPhotosIds) {
+    // 创建photoGroups的副本进行遍历,避免遍历过程中修改photoGroups
+    for (var group in List.from(controller.photoGroups)) {
+      // 移除所有与selectedPhotosIds匹配的图片
+      group.images.removeWhere((image) => selectedPhotosIds.contains(image.id));
+
+      // 如果当前组内没有图片了,删除该组
+      if (group.images.isEmpty) {
+        controller.photoGroups.remove(group);
+      }
+    }
+  }
+  static void updatePhotoData(Set<String> selectedPhotosIds1) {
 
+    debugPrint("ImagePickerUtil1 updatePhotoData  $selectedPhotosIds1");
+    Set<String> selectedPhotosIds = Set.from(selectedPhotosIds1);
+    screenshotPhotos.removeWhere((element) => selectedPhotosIds.contains(element.id));
+    for (var group in similarPhotos) {
+      group.removeWhere((element) => selectedPhotosIds.contains(element.id));
     }
+    locationPhotos.forEach((key, group) => group.removeWhere((element) => selectedPhotosIds.contains(element.id)));
+    peoplePhotos.removeWhere((element) => selectedPhotosIds.contains(element.id));
+    blurryPhotos.removeWhere((element) => selectedPhotosIds.contains(element.id));
+    // 2. 移除空的集合
+    similarPhotos.removeWhere((element) => element.isEmpty);
+    locationPhotos.removeWhere((key, value) => value.isEmpty);
+
+    debugPrint("ImagePickerUtil2 updatePhotoData  $selectedPhotosIds");
+    selectedScreenshotPhotosIds.removeWhere((element) => selectedPhotosIds.contains(element));
+    selectedSimilarPhotosIds.removeWhere((element) => selectedPhotosIds.contains(element));
+    selectedLocationPhotosIds.removeWhere((element) => selectedPhotosIds.contains(element));
+    selectedPeoplePhotosIds.removeWhere((element) => selectedPhotosIds.contains(element));
+    selectedBlurryPhotosIds.removeWhere((element) => selectedPhotosIds.contains(element));
+    debugPrint("ImagePickerUtil3  updatePhotoData $selectedPhotosIds");
+    CalendarState.removePhotosData(selectedPhotosIds);
   }
 
   // 更新照片数据
@@ -278,11 +301,16 @@ class ImagePickerUtil {
   }
 
 
-  static Future<List<AssetEntity>> loadAssetsAndroid({bool sortByDate = true}) async {
+
+  // 加载图片资源
+  static Future<List<AssetEntity>> loadAssets({bool sortByDate = true}) async {
+
     final PermissionState result = await PhotoManager.requestPermissionExtend();
     if (!result.isAuth) return [];
 
+    if (androidPhotos.isNotEmpty) return androidPhotos;
     // 选择相册
+
     final List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
       type: RequestType.image,
       filterOption: FilterOptionGroup(
@@ -298,12 +326,13 @@ class ImagePickerUtil {
 
     if (albums.isEmpty) return [];
 
+    print('相册数量:$albums');
     // 获取图片资源
-    final List<AssetEntity> assets = await albums.first.getAssetListPaged(
+   androidPhotos = await albums.first.getAssetListPaged(
       page: 0,
-      size: 10000, // 获取 10000 张图片
+      size: 10000,
     );
 
-    return assets;
+    return androidPhotos;
   }
 }

+ 28 - 17
lib/module/people_photo/photo_group.dart

@@ -2,51 +2,62 @@ import 'package:get/get.dart';
 import 'package:wechat_assets_picker/wechat_assets_picker.dart';
 
 class PhotoGroup {
-  // 照片组的标题
-  // final String title;
-
-  // // 照片组中的图片数量
-  // final int imageCount;
-
   // 照片组的选择状态
   final RxBool isSelected;
 
+
   // 照片组中的图片列表
-  final List<AssetEntity> images;
+  final RxList<AssetEntity> images;
 
   // 每张图片的选择状态
   final RxList<bool> selectedImages;
 
+  // 已选中图片的唯一标识集合
+  final RxSet<String> selectedPhotosIds = <String>{}.obs;
+
   // 照片组的位置
   final String? location;
 
+  // 选中文件的总大小
+  RxDouble selectedTotalSize = 0.0.obs;
+
   // 照片组的月份
   final String? month;
 
   // 获取已选中的图片数量
-  int get selectedCount => selectedImages.where((selected) => selected).length;
+  int get selectedCount => selectedPhotosIds.length;
+
+
 
   // 构造函数
   PhotoGroup({
-    // required this.imageCount,
-    // required this.title,
     required bool isSelected,
-    required this.images,
+    required List<AssetEntity> images,
     this.location,
     this.month,
   })  : isSelected = isSelected.obs,
-        selectedImages = RxList<bool>.filled(images.length, isSelected);
+        images = images.obs,
+
+  selectedImages = RxList<bool>.filled(images.length, isSelected) {
+    // 初始化已选中图片的唯一标识集合
+    if (isSelected) {
+      selectedPhotosIds.addAll(images.map((e) => e.id).toList());
+    }
+  }
 
   // 切换选择所有图片的状态
   void toggleSelectAll(bool value) {
     isSelected.value = value;
     selectedImages.assignAll(List.filled(images.length, value));
+    if (value) {
+      selectedPhotosIds.addAll(images.map((e) => e.id).toList());
+    } else {
+      selectedPhotosIds.clear();
+    }
   }
 
-  // 更新图片的选择状态
-  void updateImageSelection(int index, bool isSelected) {
-    if (index >= 0 && index < images.length) {
-      selectedImages[index] = isSelected;
-    }
+  /// 判断某张图片是否被选中
+  bool isImageSelected(AssetEntity image) {
+    return selectedPhotosIds.contains(image.id);
   }
 }

+ 39 - 24
lib/module/photo_preview/photo_preview_controller.dart

@@ -146,6 +146,7 @@ class PhotoPreviewController extends BaseController
 
   //删除成功清除选中的图片
   void cleanSelections() async {
+    debugPrint('PhotoPreviewController cleanSelections');
     listAssetEntity
         .removeWhere((element) => selectedPhotosIds.contains(element.id));
     selectedPhotosIds.clear();
@@ -190,31 +191,42 @@ class PhotoPreviewController extends BaseController
       selectedFilesSize.value = 0;
       return;
     }
+
     double totalSize = 0;
-    // 通过selectedPhotosIds获取选中的图片,然后计算大小
-    for (var id in selectedPhotosIds) {
-      // 检查缓存
-      if (ImagePickerUtil.fileSizeCache.containsKey(id)) {
-        totalSize += ImagePickerUtil.fileSizeCache[id]!;
-      } else {
-        final entity = await AssetEntity.fromId(id);
-        if (entity != null) {
-          final file = await entity.file;
-          if (file != null) {
-            double size = await file.length() / 1024; // 转换为KB
-            totalSize += size;
-            // 缓存文件大小
-            ImagePickerUtil.fileSizeCache[id] = size;
-          }
-        }
-      }
-    }
 
+    // 创建一个 Future 列表,用于并行获取文件大小
+    List<Future<double>> sizeFutures = selectedPhotosIds.map((id) => getFileSize(id)).toList();
+
+    // 等待所有文件大小计算完成
+    List<double> sizes = await Future.wait(sizeFutures);
+
+    // 计算总大小
+    totalSize = sizes.fold(0, (sum, size) => sum + size);
 
     selectedFilesSize.value = totalSize; // 更新总大小
     PhotoManager.clearFileCache();
   }
 
+// 获取文件大小
+  Future<double> getFileSize(String assetId) async {
+    // 先检查缓存
+    if (ImagePickerUtil.fileSizeCache.containsKey(assetId)) {
+      return ImagePickerUtil.fileSizeCache[assetId]!;
+    }
+
+    final entity = await AssetEntity.fromId(assetId);
+    if (entity != null) {
+      final file = await entity.file;
+      if (file != null) {
+        double size = await file.length() / 1024; // 转换为 KB
+        ImagePickerUtil.fileSizeCache[assetId] = size; // 缓存文件大小
+        file.delete();
+        return size;
+      }
+    }
+    return 0;
+  }
+
   void updateSelections(Set<String> selectedIds) {
     print(
         'PhotoPreviewController updateSelections photosType $photosType selectedIds $selectedIds');
@@ -449,12 +461,15 @@ class PhotoPreviewController extends BaseController
 
   // 将selectedFilesSize转成String类型,然后单位转换,如果超过1MB,则转成MB,超过1GB,则转成GB,否则KB
   String get selectedFilesSizeString {
-    if (selectedFilesSize.value > 1024) {
-      return "${(selectedFilesSize.value / 1024).toStringAsFixed(2)}MB";
-    } else if (selectedFilesSize.value > 1024 * 1024) {
-      return "${(selectedFilesSize.value / 1024 / 1024).toStringAsFixed(2)}GB";
-    } else {
-      return "${(selectedFilesSize.value).toStringAsFixed(2)}KB";
+
+    final double sizeInKB = selectedFilesSize.value;
+
+    if (sizeInKB >= 1024 * 1024) {  // 先检查最大单位(GB)
+      return "${(sizeInKB / (1024 * 1024)).toStringAsFixed(2)}GB";
+    } else if (sizeInKB >= 1024) {  // 然后检查MB
+      return "${(sizeInKB / 1024).toStringAsFixed(2)}MB";
+    } else {  // 最后是KB
+      return "${sizeInKB.toStringAsFixed(2)}KB";
     }
   }
 }

+ 1 - 0
lib/module/similar_photo/similar_photo_controller.dart

@@ -91,6 +91,7 @@ class SimilarPhotoController extends BasePhotoController {
           for (var image in imagesToRemove) {
             updateSelectedPhotosIds(image.id, false);
           }
+          ImagePickerUtil.updatePhotoData(result.toSet());
           selectedFileCount.value = selectedPhotosIds.length;
           updateSelectedFilesSize();
 

+ 6 - 4
lib/module/similar_photo/similar_photo_view.dart

@@ -206,7 +206,7 @@ class SimilarPhotoPage extends BasePage<SimilarPhotoController> {
                 onTap: () => controller.toggleGroupSelection(imagesList),
                 child: Obx(() => Text(
                       controller.photoGroups
-                              .firstWhere((g) => g.images == imagesList)
+                              .firstWhere((g) => g.images.toSet().containsAll(imagesList))
                               .isSelected
                               .value
                           ? 'Deselect All'
@@ -235,7 +235,7 @@ class SimilarPhotoPage extends BasePage<SimilarPhotoController> {
                       height: 148.h,
                       child: Obx(() {
                         final group = controller.photoGroups
-                            .firstWhere((g) => g.images == imagesList);
+                            .firstWhere((g) => g.images.toSet().containsAll(imagesList));
                         return Stack(
                           children: [
                             Container(
@@ -343,7 +343,7 @@ class SimilarPhotoPage extends BasePage<SimilarPhotoController> {
                                   controller.clickImage(imagesList, realIndex, PhotosType.similarPhotos),
                               child: Obx(() {
                                 final group = controller.photoGroups
-                                    .firstWhere((g) => g.images == imagesList);
+                                    .firstWhere((g) => g.images.toSet().containsAll(imagesList));
                                 return Container(
                                   decoration: ShapeDecoration(
                                     color: Colors.white.withValues(alpha: 0.12),
@@ -420,7 +420,9 @@ class SimilarPhotoPage extends BasePage<SimilarPhotoController> {
               ),
               child: Center(
                 child: Obx(() => Text(
-                      'Move ${controller.photoGroups.firstWhere((g) => g.images == imagesList).selectedCount} to trash',
+
+
+                      'Move ${controller.photoGroups.firstWhere((g) => g.images.toSet().containsAll(imagesList)).selectedImages.where((element) => element).length} to trash',
                       style: TextStyle(
                         color: Colors.white,
                         fontSize: 16.sp,

+ 12 - 0
lib/router/app_pages.dart

@@ -6,6 +6,8 @@ import 'package:clean/module/contact/contact_controller.dart';
 import 'package:clean/module/contact/contact_view.dart';
 import 'package:clean/module/calendar/calendar_controller.dart';
 import 'package:clean/module/calendar/calendar_month_controller.dart';
+import 'package:clean/module/calendar/preview/calendar_preview_controller.dart';
+import 'package:clean/module/calendar/selected_preview/calendar_selected_preview_controller.dart';
 import 'package:clean/module/home/home_controller.dart';
 import 'package:clean/module/locations_photo/locations_photo_controller.dart';
 import 'package:clean/module/locations_photo/locations_photo_view.dart';
@@ -42,6 +44,8 @@ import 'package:get/get.dart';
 import '../module/browser/browser_controller.dart';
 import '../module/browser/browser_view.dart';
 import '../module/calendar/calendar_month_view.dart';
+import '../module/calendar/preview/calendar_preview_view.dart';
+import '../module/calendar/selected_preview/calendar_selected_preview_view.dart';
 import '../module/main/main_controller.dart';
 import '../module/photo_info/photo_info_view.dart';
 import '../module/privacy/privacy_controller.dart';
@@ -74,6 +78,8 @@ abstract class RoutePath {
   static const contact = '/contact';
   static const contactAll = '/contact/all';
   static const calendarMonth = '/calendarMonth';
+  static const calendarPreview =  '/calendarPreview';
+  static const calendarSelectedPreview = '/calendarSelectedPreview';
 }
 
 class AppBinding extends Bindings {
@@ -103,6 +109,8 @@ class AppBinding extends Bindings {
     lazyPut(() => AllController());
     lazyPut(() => CalendarController());
     lazyPut(() => CalendarMonthController());
+    lazyPut(() => CalendarPreviewController());
+    lazyPut(() => CalendarSelectedPreviewController());
   }
 
   void lazyPut<S>(InstanceBuilderCallback<S> builder) {
@@ -136,4 +144,8 @@ final generalPages = [
   GetPage(name: RoutePath.contact, page: () => ContactPage()),
   GetPage(name: RoutePath.contactAll, page: () => AllPage()),
   GetPage(name: RoutePath.calendarMonth, page: () => CalendarMonthPage()),
+  GetPage(name: RoutePath.calendarPreview, page: () => CalendarPreviewPage()),
+  GetPage(
+      name: RoutePath.calendarSelectedPreview,
+      page: () => CalendarSelectedPreviewPage()),
 ];