Parcourir la source

添加预览页删除功能

云天逵 il y a 1 an
Parent
commit
3047b8dbe4

BIN
assets/images/bg_preview_swiper_end_firework.webp


BIN
assets/images/icon_preview_swiper_end_finish.webp


BIN
assets/images/icon_preview_swiper_end_firework.webp


+ 56 - 18
lib/module/image_picker/image_picker_util.dart

@@ -1,6 +1,7 @@
 import 'dart:io';
 import 'dart:math';
 
+import 'package:clean/data/bean/photos_type.dart';
 import 'package:clean/model/asset_group.dart';
 import 'package:clean/module/people_photo/photo_group.dart';
 import 'package:get/get.dart';
@@ -28,21 +29,14 @@ class ImagePickerUtil {
   static final RxMap<String, List<AssetEntity>> locationPhotos =
       <String, List<AssetEntity>>{}.obs;
 
-
   // 人物图片
   static final RxList<AssetEntity> peoplePhotos = <AssetEntity>[].obs;
 
-
-
-
-
-  static final  RxSet<String>  selectedScreenshotPhotosIds = <String>{}.obs;
-  static final  RxSet<String>  selectedSimilarPhotosIds = <String>{}.obs;
-  static final  RxSet<String>  selectedLocationPhotosIds = <String>{}.obs;
-  static final  RxSet<String>  selectedPeoplePhotosIds = <String>{}.obs;
-  static final  RxSet<String>  selectedBlurryPhotosIds = <String>{}.obs;
-
-
+  static final RxSet<String> selectedScreenshotPhotosIds = <String>{}.obs;
+  static final RxSet<String> selectedSimilarPhotosIds = <String>{}.obs;
+  static final RxSet<String> selectedLocationPhotosIds = <String>{}.obs;
+  static final RxSet<String> selectedPeoplePhotosIds = <String>{}.obs;
+  static final RxSet<String> selectedBlurryPhotosIds = <String>{}.obs;
 
   // 添加大小信息的变量
   static final Rx<int> screenshotsSize = 0.obs;
@@ -61,6 +55,48 @@ class ImagePickerUtil {
     peoplePhotos.clear();
   }
 
+  // 更新删除后的照片数据
+  static Future<void> updatePhotoGroupDate(PhotosType photosType) async {
+    switch (photosType) {
+      case PhotosType.screenshots:
+        // 去除包含在selectedScreenshotPhotosIds中的screenshotPhotos
+        screenshotPhotos.removeWhere((element) => selectedScreenshotPhotosIds.contains(element.id));
+        selectedScreenshotPhotosIds.clear();
+        print(screenshotPhotos);
+
+        break;
+      case PhotosType.similarPhotos:
+        // 去除包含在selectedSimilarPhotosIds中的similarPhotos
+        for (var group in similarPhotos) {
+          group.removeWhere(
+              (element) => selectedSimilarPhotosIds.contains(element.id));
+        }
+        selectedSimilarPhotosIds.clear();
+        break;
+      case PhotosType.locationPhotos:
+        // 去除包含在selectedLocationPhotosIds中的locationPhotos
+        for (var group in locationPhotos.values) {
+          group.removeWhere(
+              (element) => selectedLocationPhotosIds.contains(element.id));
+        }
+        selectedLocationPhotosIds.clear();
+
+        break;
+      case PhotosType.peoplePhotos:
+        // 去除包含在selectedPeoplePhotosIds中的peoplePhotos
+        peoplePhotos.removeWhere(
+            (element) => selectedPeoplePhotosIds.contains(element.id));
+        selectedPeoplePhotosIds.clear();
+        break;
+      case PhotosType.blurryPhotos:
+        // 去除包含在selectedBlurryPhotosIds中的blurryPhotos
+        blurryPhotos.removeWhere(
+            (element) => selectedBlurryPhotosIds.contains(element.id));
+        selectedBlurryPhotosIds.clear();
+        break;
+    }
+  }
+
   // 更新照片数据
   static Future<void> updatePhotos(
       List<Map<String, dynamic>> photoGroups) async {
@@ -187,16 +223,18 @@ class ImagePickerUtil {
 
   /// 格式化存储大小
   static String formatSize(double sizeInGB) {
-    if (sizeInGB < 0.001) { // 小于 1MB
+    if (sizeInGB < 0.001) {
+      // 小于 1MB
       return '${(sizeInGB * 1024 * 1024).toStringAsFixed(1)} ';
-    } else if (sizeInGB < 1) { // 小于 1GB
+    } else if (sizeInGB < 1) {
+      // 小于 1GB
       return '${(sizeInGB * 1024).toStringAsFixed(1)} ';
-    } else if (sizeInGB < 1024) { // 小于 1TB
+    } else if (sizeInGB < 1024) {
+      // 小于 1TB
       return '${sizeInGB.toStringAsFixed(1)} ';
-    } else { // 大于等于 1TB
+    } else {
+      // 大于等于 1TB
       return '${(sizeInGB / 1024).toStringAsFixed(1)} ';
     }
   }
-
-
 }

+ 22 - 4
lib/module/locations_photo/locations_photo_controller.dart

@@ -1,13 +1,16 @@
 import 'dart:io';
 
 import 'package:clean/module/image_picker/image_picker_util.dart';
+import 'package:clean/module/locations_photo/locations_single_photo_view.dart';
 import 'package:clean/module/people_photo/photo_group.dart';
 import 'package:get/get.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:path/path.dart' as p;
 import 'package:wechat_assets_picker/wechat_assets_picker.dart';
+
 class LocationsPhotoController extends GetxController {
   final RxList<PhotoGroup> photoGroups = <PhotoGroup>[].obs;
+
   @override
   onInit() {
     super.onInit();
@@ -16,20 +19,35 @@ class LocationsPhotoController extends GetxController {
 
   void loadLocationPhoto() {
     photoGroups.clear();
-    RxMap<String, List<AssetEntity>> locationPhotos = ImagePickerUtil.locationPhotos;
+    RxMap<String, List<AssetEntity>> locationPhotos =
+        ImagePickerUtil.locationPhotos;
 
     if (locationPhotos.isNotEmpty) {
       for (var entry in locationPhotos.entries) {
-
         photoGroups.add(PhotoGroup(
           title: 'photo: ${entry.value.length}',
           imageCount: entry.value.length,
           isSelected: false,
           images: entry.value,
-          location: entry.key.obs,
+          location: entry.key,
         ));
       }
     }
   }
 
-}
+  void clickPhotoGroup(PhotoGroup photoGroup) {
+    print('clickPhotoGroup ${photoGroup.location}');
+    LocationsSinglePhotoPage.start(photoGroup: photoGroup);
+  }
+
+  PhotoGroup getGroupByLocation(String? location) {
+    return photoGroups.firstWhere((group) => group.location == location);
+  }
+
+// PhotoGroup getGroupByImages(List<AssetEntity> images) {
+//   final imageIds = images.map((img) => img.id).toSet();
+//   return photoGroups.firstWhere((group) =>
+//       group.images.every((image) => imageIds.contains(image.id))
+//   );
+// }
+}

+ 5 - 4
lib/module/locations_photo/locations_photo_view.dart

@@ -37,6 +37,7 @@ class LocationsPhotoPage extends BasePage<LocationsPhotoController> {
                             children: [
                               _buildPhotoGroup(
                                 title: "photo: ${group.images.length}",
+                                location: group.location?? '',
                                 imageCount: group.images.length,
                               ),
                               SizedBox(height: 15.h),
@@ -89,6 +90,7 @@ class LocationsPhotoPage extends BasePage<LocationsPhotoController> {
   }
 
   Widget _buildPhotoGroup({
+    required String location,
     required String title,
     required int imageCount,
   }) {
@@ -122,12 +124,11 @@ class LocationsPhotoPage extends BasePage<LocationsPhotoController> {
           ),
           SizedBox(
             child: GestureDetector(
-              // onTap: () => controller.toggleImageSelection(title, 0),
+              onTap: () => controller.clickPhotoGroup(controller.getGroupByLocation(location)),
               child: Obx(() {
                 final group =
-                    controller.photoGroups.firstWhere((g) => g.title == title);
-                final imagePath = group.images[0];
-                final location = group.location?.value?? '';
+                    controller.getGroupByLocation(location);
+                final imagePath = group.images.first;
                 return Stack(
                   children: [
                     Container(

+ 118 - 0
lib/module/locations_photo/locations_single_photo_controller.dart

@@ -0,0 +1,118 @@
+
+import 'package:clean/base/base_controller.dart';
+import 'package:clean/data/bean/photos_type.dart';
+import 'package:clean/module/image_picker/image_picker_util.dart';
+import 'package:clean/module/locations_photo/locations_photo_controller.dart';
+import 'package:clean/module/people_photo/photo_group.dart';
+import 'package:clean/module/photo_preview/photo_preview_view.dart';
+import 'package:get/get.dart';
+import 'package:wechat_assets_picker/wechat_assets_picker.dart';
+class LocationsSinglePhotoController extends BaseController {
+
+  late final PhotoGroup photoGroup;
+  final RxInt selectedFileCount = 0.obs;
+
+
+  final RxList<PhotoGroup> photoGroups = <PhotoGroup>[].obs;
+  final RxDouble selectedFilesSize = 0.0.obs;
+  @override
+  void onInit() {
+    // TODO: implement onInit
+    super.onInit();
+    _getArgs();
+    loadLocationsSinglePhoto();
+    restoreSelections();
+
+  }
+
+  void clickImage(String location, int imageIndex) {
+    final group = getGroupByLocation(location);
+    final image = group.images[imageIndex];
+
+    PhotoPreviewPage.start(PhotosType.locationPhotos, image.id);
+  }
+
+  void toggleImageSelection(String groupTitle, int imageIndex) {
+    final group = getGroupByLocation(groupTitle);
+    final image = group.images[imageIndex];
+    final selected = !group.selectedImages[imageIndex];
+
+    group.selectedImages[imageIndex] = selected;
+    _updateSelectedScreenshotsPhotosIds(image.id, selected);
+
+    group.isSelected.value = group.selectedImages.every((selected) => selected);
+
+    updateSelectedFilesSize();
+    selectedFileCount.value = ImagePickerUtil.selectedLocationPhotosIds.length;
+  }
+
+  Future<void> updateSelectedFilesSize() async {
+    double totalSize = 0;
+
+    for (var id in ImagePickerUtil.selectedLocationPhotosIds) {
+      final entity = await AssetEntity.fromId(id);
+      if (entity != null) {
+        final file = await entity.file;
+        if (file != null) {
+          totalSize += await file.length();
+        }
+      }
+    }
+
+    selectedFilesSize.value = totalSize / 1024; // Convert to KB
+  }
+
+  void _updateSelectedScreenshotsPhotosIds(String photoId, bool isSelected) {
+    if (isSelected) {
+      ImagePickerUtil.selectedLocationPhotosIds.add(photoId);
+    } else {
+      ImagePickerUtil.selectedLocationPhotosIds.remove(photoId);
+    }
+  }
+
+  // 获取参数
+  void _getArgs() {
+    photoGroup = parameters?['PhotoGroup'];
+
+  }
+  void loadLocationsSinglePhoto() {
+    photoGroups.clear();
+    photoGroups.add(photoGroup);
+  }
+
+  void restoreSelections() async {
+    final selectedIds = ImagePickerUtil.selectedLocationPhotosIds.toSet();
+    for (var group in photoGroups) {
+      for (int i = 0; i < group.images.length; i++) {
+        group.selectedImages[i] = selectedIds.contains(group.images[i].id);
+      }
+      group.isSelected.value = group.selectedImages.every((selected) => selected);
+    }
+    await updateSelectedFilesSize();
+    selectedFileCount.value = selectedIds.length;
+  }
+
+  void toggleGroupSelection(String location) {
+    final group = getGroupByLocation(location);
+    final newValue = !group.isSelected.value;
+    group.toggleSelectAll(newValue);
+
+    for (var image in group.images) {
+      _updateSelectedScreenshotsPhotosIds(image.id, newValue);
+    }
+
+    updateSelectedFilesSize();
+    selectedFileCount.value =  ImagePickerUtil.selectedLocationPhotosIds.length;
+  }
+
+  // 通过标题获取照片组
+  PhotoGroup _getGroupByTitle(String groupTitle) {
+    return photoGroups.firstWhere((g) => g.title == groupTitle);
+  }
+
+  // 通过位置获取照片组
+  PhotoGroup getGroupByLocation(String? location) {
+    return photoGroups.firstWhere((group) => group.location == location);
+  }
+
+}

+ 255 - 0
lib/module/locations_photo/locations_single_photo_view.dart

@@ -0,0 +1,255 @@
+import 'package:clean/base/base_page.dart';
+import 'package:clean/module/locations_photo/locations_single_photo_controller.dart';
+import 'package:clean/module/people_photo/photo_group.dart';
+import 'package:clean/resource/assets.gen.dart';
+import 'package:clean/router/app_pages.dart';
+import 'package:flutter/Material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import 'package:wechat_assets_picker/wechat_assets_picker.dart';
+
+class LocationsSinglePhotoPage
+    extends BasePage<LocationsSinglePhotoController> {
+  const LocationsSinglePhotoPage({super.key});
+
+  static void start({required PhotoGroup photoGroup}) {
+    Get.toNamed(RoutePath.locationsSinglePhoto, arguments: {
+      "PhotoGroup": photoGroup,
+    });
+  }
+
+  @override
+  bool statusBarDarkFont() {
+    return false;
+  }
+
+  @override
+  bool immersive() {
+    return true;
+  }
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Stack(children: [
+      Container(
+        child: SafeArea(
+          child: Column(
+            children: [
+              _titleCard(),
+              // Photo groups
+              Expanded(
+                child: Obx(() {
+                  return SizedBox(
+                    child: GridView.builder(
+                        padding: EdgeInsets.symmetric(horizontal: 16.w),
+                        scrollDirection: Axis.vertical,
+                        // 设置为垂直方向滚动
+                        physics: BouncingScrollPhysics(),
+                        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+                          crossAxisCount: 3, // 每行显示 3 个元素
+                          mainAxisSpacing: 8.w, // 垂直间距
+                          crossAxisSpacing: 8.h, // 水平间距
+                        ),
+                        itemCount: controller.photoGroups.first.imageCount,
+                        itemBuilder: _buildPhotoItem(
+                            controller.photoGroups.first.location ?? "")),
+                  );
+                }),
+              ),
+
+              _bottomBarCard(),
+              SizedBox(height: 8.h),
+            ],
+          ),
+        ),
+      ),
+      IgnorePointer(
+        child: Assets.images.bgHome.image(
+          width: 360.w,
+          height: 234.h,
+        ),
+      ),
+    ]);
+  }
+
+  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: () => Get.back(),
+                child: Assets.images.iconBackArrow.image(
+                  width: 28.w,
+                  height: 28.h,
+                ),
+              ),
+             Text(
+                controller.photoGroups.first.location ?? "",
+                textAlign: TextAlign.center,
+                style: TextStyle(
+                  color: Colors.white,
+                  fontSize: 17.sp,
+                  fontWeight: FontWeight.w500,
+                ),
+              ),
+              GestureDetector(
+                onTap: () => controller
+                    .toggleGroupSelection(controller.photoGroups.first.location?? ""),
+                child: Obx(() => Text(
+                      controller.photoGroups.first.isSelected.value
+                          ? 'Deselect All'
+                          : 'Select All',
+                      style: TextStyle(
+                        color: Colors.white.withValues(alpha: 0.7),
+                        fontSize: 14.sp,
+                        fontWeight: FontWeight.w400,
+                      ),
+                    )),
+              ),
+            ],
+          ),
+          SizedBox(height: 30.h),
+        ],
+      ),
+    );
+  }
+
+  Widget _bottomBarCard() {
+    return 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(() {
+            return Text(
+              '${controller.selectedFileCount.value} files selected (${controller.selectedFilesSize.value.toStringAsFixed(1)} KB)',
+              textAlign: TextAlign.center,
+              style: TextStyle(
+                color: Colors.white,
+                fontSize: 16.sp,
+                fontWeight: FontWeight.w500,
+              ),
+            );
+          }),
+        ],
+      ),
+    );
+  }
+
+  Widget Function(BuildContext, int) _buildPhotoItem(String location) =>
+      (context, index) {
+        final group = controller.getGroupByLocation(location);
+        final assetEntity = group.images[index];
+
+        return GestureDetector(
+          onTap: () => controller.clickImage(location, 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(assetEntity),
+                      fit: BoxFit.cover,
+                    ),
+                  ),
+                ),
+                Positioned(
+                  right: 8.w,
+                  bottom: 8.h,
+                  child: GestureDetector(
+                      onTap: () =>
+                          controller.toggleImageSelection(location, 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(
+      children: [
+        _titleCard(),
+        Expanded(
+          child: Center(
+            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,
+                  ),
+                ),
+              ],
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+}

+ 2 - 1
lib/module/people_photo/photo_group.dart

@@ -17,7 +17,7 @@ class PhotoGroup {
   final RxList<bool> selectedImages;
 
   // 照片组的位置
-  final RxString? location;
+  final String? location;
 
   int get selectedCount => selectedImages.where((selected) => selected).length;
 
@@ -34,4 +34,5 @@ class PhotoGroup {
     isSelected.value = value;
     selectedImages.assignAll(List.generate(images.length, (_) => value));
   }
+
 }

+ 90 - 19
lib/module/photo_preview/photo_preview_controller.dart

@@ -3,21 +3,22 @@ import 'dart:async';
 import 'package:clean/base/base_controller.dart';
 import 'package:clean/data/bean/photos_type.dart';
 import 'package:clean/module/image_picker/image_picker_util.dart';
+import 'package:clean/module/locations_photo/locations_single_photo_controller.dart';
 import 'package:clean/module/people_photo/people_photo_controller.dart';
 import 'package:clean/module/people_photo/photo_group.dart';
 import 'package:clean/module/screenshots_blurry/screenshots_controller.dart';
 import 'package:clean/module/similar_photo/similar_photo_controller.dart';
+import 'package:clean/utils/file_utils.dart';
 import 'package:flutter/Material.dart';
 import 'package:flutter_card_swiper/flutter_card_swiper.dart';
 import 'package:get/get.dart';
 import 'package:wechat_assets_picker/wechat_assets_picker.dart';
 
-
 class PhotoPreviewController extends BaseController {
   Rx<CardSwiperController> cardSwiperController = CardSwiperController().obs;
   final RxList<AssetEntity> photoGroups = <AssetEntity>[].obs;
   final RxSet<String> selectedPhotosIds = <String>{}.obs;
-
+  final RxBool isSwiperEnd = false.obs;
   RxInt groupIndex = 0.obs;
 
   late PhotosType photosType;
@@ -28,7 +29,7 @@ class PhotoPreviewController extends BaseController {
   @override
   void onInit() {
     super.onInit();
-
+    isSwiperEnd.value = false;
     print('PhotoPreviewController onInit');
     _getArgs(); // 获取传递的参数
     _initData(); // 初始化数据
@@ -57,39 +58,44 @@ class PhotoPreviewController extends BaseController {
     photoGroups.clear();
     selectedPhotosIds.clear();
 
-    switch(photosType) {
+    switch (photosType) {
       case PhotosType.peoplePhotos:
         photoGroups.assignAll(ImagePickerUtil.peoplePhotos);
         selectedPhotosIds.assignAll(ImagePickerUtil.selectedPeoplePhotosIds);
-        selectedFileCount.value = ImagePickerUtil.selectedPeoplePhotosIds.length;
+        selectedFileCount.value =
+            ImagePickerUtil.selectedPeoplePhotosIds.length;
         break;
       case PhotosType.screenshots:
         photoGroups.assignAll(ImagePickerUtil.screenshotPhotos);
-        selectedPhotosIds.assignAll(ImagePickerUtil.selectedScreenshotPhotosIds);
-        selectedFileCount.value = ImagePickerUtil.selectedScreenshotPhotosIds.length;
+        selectedPhotosIds
+            .assignAll(ImagePickerUtil.selectedScreenshotPhotosIds);
+        selectedFileCount.value =
+            ImagePickerUtil.selectedScreenshotPhotosIds.length;
         break;
       case PhotosType.similarPhotos:
         for (var group in ImagePickerUtil.similarPhotos) {
           photoGroups.addAll(group);
         }
         selectedPhotosIds.assignAll(ImagePickerUtil.selectedSimilarPhotosIds);
-        selectedFileCount.value = ImagePickerUtil.selectedSimilarPhotosIds.length;
+        selectedFileCount.value =
+            ImagePickerUtil.selectedSimilarPhotosIds.length;
         break;
       case PhotosType.locationPhotos:
         for (var group in ImagePickerUtil.locationPhotos.values) {
           photoGroups.addAll(group);
         }
         selectedPhotosIds.assignAll(ImagePickerUtil.selectedLocationPhotosIds);
-        selectedFileCount.value = ImagePickerUtil.selectedLocationPhotosIds.length;
+        selectedFileCount.value =
+            ImagePickerUtil.selectedLocationPhotosIds.length;
         break;
-        case PhotosType.blurryPhotos:
+      case PhotosType.blurryPhotos:
         photoGroups.assignAll(ImagePickerUtil.blurryPhotos);
         selectedPhotosIds.assignAll(ImagePickerUtil.selectedBlurryPhotosIds);
-        selectedFileCount.value = ImagePickerUtil.selectedBlurryPhotosIds.length;
+        selectedFileCount.value =
+            ImagePickerUtil.selectedBlurryPhotosIds.length;
         break;
     }
     updateSelectedFilesSize();
-
   }
 
   Future<void> updateSelectedFilesSize() async {
@@ -109,12 +115,11 @@ class PhotoPreviewController extends BaseController {
   }
 
   void recoverSelectPhoto() {
+
     cardSwiperController.value.undo();
   }
 
   void clickSelect() {
-
-
     cardSwiperController.value.swipe(CardSwiperDirection.left);
   }
 
@@ -124,11 +129,9 @@ class PhotoPreviewController extends BaseController {
 
     // 清理操作,释放资源
     cardSwiperController.value.dispose();
-
   }
 
   void clickBack() {
-
     _saveSelectedPhotos(photosType);
     Get.back();
   }
@@ -139,28 +142,38 @@ class PhotoPreviewController extends BaseController {
       case PhotosType.peoplePhotos:
         ImagePickerUtil.selectedPeoplePhotosIds.assignAll(selectedPhotosIds);
         PeoplePhotoController controller = Get.find<PeoplePhotoController>();
+        controller.loadPeoplePhoto();
         controller.restoreSelections();
         break;
       case PhotosType.screenshots:
-        ImagePickerUtil.selectedScreenshotPhotosIds.assignAll(selectedPhotosIds);
+        ImagePickerUtil.selectedScreenshotPhotosIds
+            .assignAll(selectedPhotosIds);
         ScreenShotsController controller = Get.find<ScreenShotsController>();
+        controller.loadScreenshots();
         controller.restoreSelections();
         break;
       case PhotosType.similarPhotos:
         ImagePickerUtil.selectedSimilarPhotosIds.assignAll(selectedPhotosIds);
         SimilarPhotoController controller = Get.find<SimilarPhotoController>();
+        controller.loadSimilarPhotos();
         controller.restoreSelections();
         break;
       case PhotosType.locationPhotos:
         ImagePickerUtil.selectedLocationPhotosIds.assignAll(selectedPhotosIds);
+        LocationsSinglePhotoController controller =
+            Get.find<LocationsSinglePhotoController>();
+        controller.loadLocationsSinglePhoto();
+        controller.restoreSelections();
         break;
       case PhotosType.blurryPhotos:
         ImagePickerUtil.selectedBlurryPhotosIds.assignAll(selectedPhotosIds);
         ScreenShotsController controller = Get.find<ScreenShotsController>();
+        controller.loadScreenshots();
         controller.restoreSelections();
         break;
-      }
+    }
   }
+
   void clickUnselect() {
     print("clickUnselect");
     cardSwiperController.value.swipe(CardSwiperDirection.right);
@@ -219,5 +232,63 @@ class PhotoPreviewController extends BaseController {
     updateSelectedFilesSize();
     return true;
   }
-}
 
+  onSwiperEnd() {
+    isSwiperEnd.value = true;
+    print('onSwiperEnd');
+  }
+
+  clickDelete() async{
+    print('clickDelete');
+    switch(photosType){
+      case PhotosType.peoplePhotos:
+        ImagePickerUtil.selectedPeoplePhotosIds.assignAll(selectedPhotosIds);
+        break;
+      case PhotosType.screenshots:
+        ImagePickerUtil.selectedScreenshotPhotosIds.assignAll(selectedPhotosIds);
+        break;
+      case PhotosType.similarPhotos:
+        ImagePickerUtil.selectedSimilarPhotosIds.assignAll(selectedPhotosIds);
+        break;
+      case PhotosType.locationPhotos:
+        ImagePickerUtil.selectedLocationPhotosIds.assignAll(selectedPhotosIds);
+        break;
+      case PhotosType.blurryPhotos:
+        ImagePickerUtil.selectedBlurryPhotosIds.assignAll(selectedPhotosIds);
+        break;
+    }
+    if (selectedPhotosIds.isNotEmpty) {
+      // 获取要删除的资产
+      final assetsToDelete = photoGroups.where(
+        (asset) => selectedPhotosIds.contains(asset.id),
+      ).toList();
+
+      // 调用方法会返回被删除的资源,如果全部失败会返回空列表。
+      final List<String> result = await PhotoManager.editor.deleteWithIds(
+        assetsToDelete.map((e) => e.id).toList(),
+      );
+      print ('PhotoPreviewController result $result');
+    }
+
+    switch(photosType){
+      case PhotosType.peoplePhotos:
+        ImagePickerUtil.updatePhotoGroupDate(PhotosType.peoplePhotos);
+        break;
+      case PhotosType.screenshots:
+        ImagePickerUtil.updatePhotoGroupDate(PhotosType.screenshots);
+        break;
+      case PhotosType.similarPhotos:
+        ImagePickerUtil.updatePhotoGroupDate(PhotosType.similarPhotos);
+        break;
+      case PhotosType.locationPhotos:
+        ImagePickerUtil.updatePhotoGroupDate(PhotosType.locationPhotos);
+        break;
+      case PhotosType.blurryPhotos:
+        ImagePickerUtil.updatePhotoGroupDate(PhotosType.blurryPhotos);
+        break;
+    }
+    _initData();
+
+  }
+
+}

+ 175 - 100
lib/module/photo_preview/photo_preview_view.dart

@@ -10,7 +10,6 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
 import 'package:wechat_assets_picker/wechat_assets_picker.dart';
 
-
 class PhotoPreviewPage extends BasePage<PhotoPreviewController> {
   PhotoPreviewPage({Key? key}) : super(key: key);
 
@@ -39,52 +38,14 @@ class PhotoPreviewPage extends BasePage<PhotoPreviewController> {
     return Stack(children: [
       Container(
         child: SafeArea(
-          child: Column(
-            children: [
-              _titleCard(),
-              Spacer(),
-              (controller.photoGroups.length == 1)
-                  ? Container(
-                      width: 314.w,
-                      height: 392.h,
-                      child: AssetEntityImage(
-                        controller.photoGroups[0],
-                        width: 314.w,
-                        height: 392.h,
-                      ),
-                    )
-                  : Container(
-                      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.value,
-                        cardsCount: controller.photoGroups.length,
-                        onSwipe: controller.onSwipe,
-                        onUndo: controller.onSwiperUndo,
-                        cardBuilder: (context,
-                            index,
-                            horizontalOffsetPercentage,
-                            verticalOffsetPercentage) {
-                          final assetEntity = controller.photoGroups[index];
-                          return AssetEntityImage(
-                            assetEntity,
-                            width: 314.w,
-                            height: 392.h,
-                          );
-                        },
-                      ),
-                    ),
-              Spacer(),
-              bottomButtonCard(),
-              _bottomBarCard(),
-            ],
+          child: Container(
+            child: Obx(() {
+              if (controller.isSwiperEnd.value) {
+                return onSwiperEndCard();
+              } else {
+                return _photoDataCard();
+              }
+            }),
           ),
         ),
       ),
@@ -97,6 +58,55 @@ class PhotoPreviewPage extends BasePage<PhotoPreviewController> {
     ]);
   }
 
+  Widget _photoDataCard() {
+    return Column(
+      children: [
+        _titleCard(),
+        Spacer(),
+        (controller.photoGroups.length == 1)
+            ? Container(
+                width: 314.w,
+                height: 392.h,
+                child: AssetEntityImage(
+                  controller.photoGroups[0],
+                  width: 314.w,
+                  height: 392.h,
+                ),
+              )
+            : Container(
+                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.value,
+                  cardsCount: controller.photoGroups.length,
+                  onSwipe: controller.onSwipe,
+                  onUndo: controller.onSwiperUndo,
+                  onEnd: controller.onSwiperEnd,
+                  cardBuilder: (context, index, horizontalOffsetPercentage,
+                      verticalOffsetPercentage) {
+                    final assetEntity = controller.photoGroups[index];
+                    return AssetEntityImage(
+                      assetEntity,
+                      width: 314.w,
+                      height: 392.h,
+                    );
+                  },
+                ),
+              ),
+        Spacer(),
+        bottomButtonCard(),
+        _bottomBarCard(),
+      ],
+    );
+  }
+
   Widget _titleCard() {
     return Container(
       alignment: Alignment.centerLeft,
@@ -153,64 +163,67 @@ class PhotoPreviewPage extends BasePage<PhotoPreviewController> {
   }
 
   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),
+    return GestureDetector(
+      onTap: controller.clickDelete,
+      child: 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.selectedFileCount.value} files selected (${controller.selectedFilesSize.value.toStringAsFixed(1)} KB)',
-              textAlign: TextAlign.center,
-              style: TextStyle(
-                color: Colors.white.withValues(alpha: 0.9),
-                fontSize: 13.sp,
-                fontWeight: FontWeight.w500,
-              ),
-            );
-          }),
-          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,
-                  ),
+        child: Row(
+          mainAxisAlignment: MainAxisAlignment.spaceBetween,
+          children: [
+            Obx(() {
+              return Text(
+                '${controller.selectedFileCount.value} files selected (${controller.selectedFilesSize.value.toStringAsFixed(1)} KB)',
+                textAlign: TextAlign.center,
+                style: TextStyle(
+                  color: Colors.white.withValues(alpha: 0.9),
+                  fontSize: 13.sp,
+                  fontWeight: FontWeight.w500,
                 ),
-                Assets.images.iconDelete.image(
-                  width: 18.w,
-                  height: 18.h,
+              );
+            }),
+            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,
+                  ),
+                ],
+              ),
             ),
-          ),
-        ],
+          ],
+        ),
       ),
     );
   }
@@ -239,4 +252,66 @@ class PhotoPreviewPage extends BasePage<PhotoPreviewController> {
       ),
     );
   }
+
+  Widget onSwiperEndCard() {
+    return Column(
+      children: [
+        Spacer(),
+        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(),
+        _bottomBarCard(),
+      ],
+    );
+  }
 }

+ 3 - 0
lib/module/screenshots_blurry/screenshots_controller.dart

@@ -28,6 +28,7 @@ class ScreenShotsController extends BaseController {
     selectedFilesSize.value = totalSize / 1024; // Convert to KB
   }
 
+
   void toggleImageSelection(String groupTitle, int imageIndex) {
     final group = _getGroupByTitle(groupTitle);
     final image = group.images[imageIndex];
@@ -48,8 +49,10 @@ class ScreenShotsController extends BaseController {
     print('clickImage $groupTitle $imageIndex');
 
     if (titleName == "Screenshots") {
+      ImagePickerUtil.selectedScreenshotPhotosIds.assignAll(selectedPhotosIds);
       PhotoPreviewPage.start(PhotosType.screenshots, image.id);
     } else if (titleName == "Blurry") {
+      ImagePickerUtil.selectedBlurryPhotosIds.assignAll(selectedPhotosIds);
       PhotoPreviewPage.start(PhotosType.blurryPhotos, image.id);
     }
 

+ 5 - 1
lib/router/app_pages.dart

@@ -1,6 +1,8 @@
 import 'package:clean/module/home/home_controller.dart';
 import 'package:clean/module/locations_photo/locations_photo_controller.dart';
 import 'package:clean/module/locations_photo/locations_photo_view.dart';
+import 'package:clean/module/locations_photo/locations_single_photo_controller.dart';
+import 'package:clean/module/locations_photo/locations_single_photo_view.dart';
 import 'package:clean/module/main/main_view.dart';
 import 'package:clean/module/photo_preview/photo_preview_controller.dart';
 import 'package:clean/module/photo_preview/photo_preview_view.dart';
@@ -35,6 +37,7 @@ abstract class RoutePath {
   static const screenshots = '/screenshots';
   static const photoInfo = '/photoInfo';
   static const photoPreview = '/photoPreview';
+  static const locationsSinglePhoto = '/locationsSinglePhoto';
 }
 
 class AppBinding extends Bindings {
@@ -49,8 +52,8 @@ class AppBinding extends Bindings {
     lazyPut(() => ScreenShotsController());
     lazyPut(() => PhotoPreviewController());
 
-    lazyPut(() => LocationsPhotoPage());
     lazyPut(() => PhotoInfoController());
+    lazyPut(() => LocationsSinglePhotoController());
   }
 
   void lazyPut<S>(InstanceBuilderCallback<S> builder) {
@@ -68,4 +71,5 @@ final generalPages = [
   GetPage(name: RoutePath.screenshots, page: () => ScreenshotsPage()),
   GetPage(name: RoutePath.photoPreview, page: () => PhotoPreviewPage()),
   GetPage(name: RoutePath.photoInfo, page: () => PhotoInfoPage()),
+  GetPage(name: RoutePath.locationsSinglePhoto, page: () => LocationsSinglePhotoPage()),
 ];