瀏覽代碼

完成people,location,similar的ui界面

云天逵 1 年之前
父節點
當前提交
32b0f8ad25

二進制
assets/images/icon_back_arrow.webp


二進制
assets/images/icon_delete.webp


二進制
assets/images/icon_selected.webp


二進制
assets/images/icon_similar_best.webp


二進制
assets/images/icon_unselected.webp


+ 9 - 5
lib/module/home/home_controller.dart

@@ -1,4 +1,7 @@
 import 'package:clean/base/base_controller.dart';
+import 'package:clean/module/locations_photo/locations_photo_view.dart';
+import 'package:clean/module/people_photo/people_photo_view.dart';
+import 'package:clean/module/similar_photo/similar_photo_view.dart';
 import 'package:get/get.dart';
 class HomeController extends BaseController {
 
@@ -21,21 +24,22 @@ class HomeController extends BaseController {
   RxInt imageCount = 0.obs;
   similarCleanClick() {
     print('similarCleanClick');
-    if (imageCount.value < 4) {
-      similarImages[imageCount.value] = 'iconMoreWallpaper'; // 第一次点击显示 'photo1'
-      imageCount.value++;
 
-    }
+    SimilarPhotoPage.start();
   }
   peopleCleanClick() {
-
+    PeoplePhotoPage.start();
     print('peopleCleanClick');
   }
   locationCleanClick() {
+
+    LocationsPhotoPage.start();
     print('locationCleanClick');
   }
   screenshotCleanClick() {
     print('screenshotCleanClick');
+
+
   }
   blurryCleanClick() {
     print('blurCleanClick');

+ 75 - 67
lib/module/home/home_view.dart

@@ -1,5 +1,3 @@
-import 'dart:ui';
-
 import 'package:clean/base/base_view.dart';
 import 'package:clean/module/home/home_controller.dart';
 import 'package:clean/resource/assets.gen.dart';
@@ -30,17 +28,17 @@ class HomePage extends BaseView<HomeController> {
         SafeArea(
           child: SingleChildScrollView(
               child: Column(
-                children: [
-                  titleCard(),
-                  storageCard(),
-                  similarCard(),
-                  quickPhotoCard(),
-                  peopleCard(),
-                  locationsCard(),
-                  screenshotsAndBlurryCard(),
-                  SizedBox(height: 40.h),
-                ],
-              )),
+            children: [
+              titleCard(),
+              storageCard(),
+              similarCard(),
+              quickPhotoCard(),
+              peopleCard(),
+              locationsCard(),
+              screenshotsAndBlurryCard(),
+              SizedBox(height: 40.h),
+            ],
+          )),
         ),
         IgnorePointer(
           child: Assets.images.bgHome.image(
@@ -87,7 +85,7 @@ class HomePage extends BaseView<HomeController> {
         width: 328.w,
         height: 146.h,
         decoration: ShapeDecoration(
-          color: Colors.white.withValues(alpha: 0.10000000149011612),
+          color: Colors.white.withValues(alpha: 0.10),
           shape: RoundedRectangleBorder(
             borderRadius: BorderRadius.circular(14.r),
           ),
@@ -141,51 +139,50 @@ class HomePage extends BaseView<HomeController> {
             CircularChartAnnotation(
               widget: Container(
                   child: Column(
-                    mainAxisSize: MainAxisSize.min,
-                    crossAxisAlignment: CrossAxisAlignment.center,
+                mainAxisSize: MainAxisSize.min,
+                crossAxisAlignment: CrossAxisAlignment.center,
+                children: [
+                  Row(
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    crossAxisAlignment: CrossAxisAlignment.end,
                     children: [
-                      Row(
-                        mainAxisAlignment: MainAxisAlignment.center,
-                        crossAxisAlignment: CrossAxisAlignment.end,
-                        children: [
-                          Obx(() {
-                            return Text(
-                              controller.usedSpacePercentage.toStringAsFixed(0),
-                              textAlign: TextAlign.end,
-                              style: TextStyle(
-                                color: Colors.white
-                                    .withValues(alpha: 0.8999999761581421),
-                                fontSize: 30.sp,
-                                height: 1,
-                                fontWeight: FontWeight.w400,
-                              ),
-                            );
-                          }),
-                          Text(
-                            '%',
-                            textAlign: TextAlign.end,
-                            style: TextStyle(
-                              color: Colors.white
-                                  .withValues(alpha: 0.8999999761581421),
-                              fontSize: 14.87.r,
-                              fontWeight: FontWeight.w500,
-                            ),
+                      Obx(() {
+                        return Text(
+                          controller.usedSpacePercentage.toStringAsFixed(0),
+                          textAlign: TextAlign.end,
+                          style: TextStyle(
+                            color: Colors.white
+                                .withValues(alpha: 0.8999999761581421),
+                            fontSize: 30.sp,
+                            height: 1,
+                            fontWeight: FontWeight.w400,
                           ),
-                        ],
-                      ),
+                        );
+                      }),
                       Text(
-                        'used',
-                        textAlign: TextAlign.center,
+                        '%',
+                        textAlign: TextAlign.end,
                         style: TextStyle(
-                          color: Colors.white.withValues(
-                              alpha: 0.6000000238418579),
+                          color: Colors.white
+                              .withValues(alpha: 0.8999999761581421),
                           fontSize: 14.87.r,
-                          height: 1,
                           fontWeight: FontWeight.w500,
                         ),
-                      )
+                      ),
                     ],
-                  )),
+                  ),
+                  Text(
+                    'used',
+                    textAlign: TextAlign.center,
+                    style: TextStyle(
+                      color: Colors.white.withValues(alpha: 0.6000000238418579),
+                      fontSize: 14.87.r,
+                      height: 1,
+                      fontWeight: FontWeight.w500,
+                    ),
+                  )
+                ],
+              )),
               horizontalAlignment: ChartAlignment.center,
               verticalAlignment: ChartAlignment.center,
               radius: '0%',
@@ -254,7 +251,7 @@ class HomePage extends BaseView<HomeController> {
                       text: controller.totalSpace.toStringAsFixed(1),
                       style: TextStyle(
                         color:
-                        Colors.white.withValues(alpha: 0.6000000238418579),
+                            Colors.white.withValues(alpha: 0.6000000238418579),
                         fontSize: 13.sp,
                         fontWeight: FontWeight.w400,
                       ),
@@ -263,7 +260,7 @@ class HomePage extends BaseView<HomeController> {
                       text: 'GB',
                       style: TextStyle(
                         color:
-                        Colors.white.withValues(alpha: 0.6000000238418579),
+                            Colors.white.withValues(alpha: 0.6000000238418579),
                         fontSize: 13.sp,
                         fontWeight: FontWeight.w500,
                       ),
@@ -371,7 +368,7 @@ class HomePage extends BaseView<HomeController> {
       margin: EdgeInsets.only(top: 20.h),
       padding: EdgeInsets.symmetric(horizontal: 16.w),
       decoration: ShapeDecoration(
-        color: Colors.white.withValues(alpha: 0.11999999731779099),
+        color: Colors.white.withValues(alpha: 0.12),
         shape: RoundedRectangleBorder(
           borderRadius: BorderRadius.circular(16.r),
         ),
@@ -678,17 +675,17 @@ class CleanUpButton extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return Container(
-        width: 94.w,
-        height: 44.h,
-        decoration: ShapeDecoration(
-          color: Color(0xFF0279FB),
-          shape: RoundedRectangleBorder(
-            borderRadius: BorderRadius.circular(10.r),
+    return GestureDetector(
+        onTap: onTap,
+        child: Container(
+          width: 94.w,
+          height: 44.h,
+          decoration: ShapeDecoration(
+            color: Color(0xFF0279FB),
+            shape: RoundedRectangleBorder(
+              borderRadius: BorderRadius.circular(10.r),
+            ),
           ),
-        ),
-        child: GestureDetector(
-          onTap: onTap,
           child: Row(
             mainAxisAlignment: MainAxisAlignment.spaceAround,
             children: [
@@ -748,9 +745,20 @@ class ImageContainer extends StatelessWidget {
         ),
       ),
       child: Center(
-        child: Assets.images.iconHomeNoPhoto.image(
-          width: size * 0.45, // 图片的大小相对容器
-          height: size * 0.45,
+        child: Center(
+          child: imagePath == 'iconHomeNoPhoto' // 如果是占位图,则显示默认图
+              ? Assets.images.iconHomeNoPhoto.image(
+                  // 显示占位图
+                  width: size * 0.45,
+                  height: size * 0.45,
+                )
+              : Image.asset(
+                  // 显示动态图片
+                  imagePath,
+                  width: size,
+                  height: size,
+                  fit: BoxFit.cover, // 保持图片的比例
+                ),
         ),
       ),
     );

+ 52 - 0
lib/module/locations_photo/locations_photo_controller.dart

@@ -0,0 +1,52 @@
+import 'dart:io';
+
+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;
+class LocationsPhotoController extends GetxController {
+  final RxList<PhotoGroup> photoGroups = <PhotoGroup>[].obs;
+  @override
+  onInit() {
+    super.onInit();
+    loadPhotosFromDirectory();
+  }
+  Future<void> loadPhotosFromDirectory() async {
+    try {
+      final Directory appDir = await getApplicationDocumentsDirectory();
+      final String photosPath = '${appDir.path}/photos';
+
+      final Directory photosDir = Directory(photosPath);
+      if (!await photosDir.exists()) {
+        return;
+      }
+
+      final List<Directory> subDirs = await photosDir
+          .list()
+          .where((entity) => entity is Directory)
+          .map((e) => e as Directory)
+          .toList();
+
+      for (final dir in subDirs) {
+        final List<FileSystemEntity> files = await dir
+            .list()
+            .where((entity) =>
+        entity is File &&
+            ['.jpg', '.jpeg', '.png']
+                .contains(p.extension(entity.path).toLowerCase()))
+            .toList();
+
+        if (files.isNotEmpty) {
+          photoGroups.add(PhotoGroup(
+            title: 'photo: ${files.length}',
+            imageCount: files.length,
+            isSelected: false,
+            images: files.map((file) => file.path).toList(),
+          ));
+        }
+      }
+    } catch (e) {
+      print('Error loading photos: $e');
+    }
+  }
+}

+ 169 - 0
lib/module/locations_photo/locations_photo_view.dart

@@ -0,0 +1,169 @@
+import 'dart:io';
+
+import 'package:clean/base/base_page.dart';
+import 'package:clean/module/locations_photo/locations_photo_controller.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';
+
+class LocationsPhotoPage extends BasePage<LocationsPhotoController> {
+  @override
+  bool statusBarDarkFont() => false;
+
+  @override
+  bool immersive() => true;
+
+  static void start() {
+    Get.toNamed(RoutePath.locationsPhoto);
+  }
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Stack(children: [
+      Container(
+        child: SafeArea(
+          child: Column(
+            children: [
+              _titleCard(),
+              Expanded(
+                child: Obx(() {
+                  return ListView(
+                    padding: EdgeInsets.symmetric(horizontal: 16.w),
+                    children: [
+                      ...controller.photoGroups.map((group) => Column(
+                        children: [
+                          _buildPhotoGroup(
+                            title: group.title,
+                            imageCount: group.imageCount,
+                          ),
+                          SizedBox(height: 15.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),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          GestureDetector(
+            onTap: () => Get.back(),
+            child: Assets.images.iconBackArrow.image(
+              width: 28.w,
+              height: 28.h,
+            ),
+          ),
+          SizedBox(height: 12.h),
+          Text(
+            'Places Photos',
+            style: TextStyle(
+              color: Colors.white,
+              fontSize: 24.sp,
+              fontWeight: FontWeight.w700,
+            ),
+          ),
+          SizedBox(height: 20.h),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildPhotoGroup({
+    required String title,
+    required int imageCount,
+  }) {
+    return Container(
+      margin: EdgeInsets.only(top: 14.h),
+      width: 328.w,
+      height: 227.h,
+      decoration: ShapeDecoration(
+        color: Colors.white.withValues(alpha: 0.12),
+        shape: RoundedRectangleBorder(
+          borderRadius: BorderRadius.circular(14.sp),
+        ),
+      ),
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+        children: [
+          Row(
+            mainAxisAlignment: MainAxisAlignment.start,
+            children: [
+              SizedBox(width: 16.w),
+              Text(
+                title,
+                textAlign: TextAlign.center,
+                style: TextStyle(
+                  color: Colors.white,
+                  fontSize: 14.sp,
+                  fontWeight: FontWeight.w500,
+                ),
+              ),
+            ],
+          ),
+          SizedBox(child:
+            GestureDetector(
+              // onTap: () => controller.toggleImageSelection(title, 0),
+              child: Obx(() {
+                final group = controller.photoGroups
+                    .firstWhere((g) => g.title == title);
+                final imagePath = group.images[0];
+                return Stack(
+                  children: [
+                    Container(
+                      width: 304.w,
+                      height: 171.h,
+                      decoration: ShapeDecoration(
+                        color: Colors.white.withValues(alpha: 0.12),
+                        shape: RoundedRectangleBorder(
+                          borderRadius: BorderRadius.circular(8.r),
+                        ),
+                        image: DecorationImage(
+                          image: FileImage(File(imagePath)),
+                          fit: BoxFit.cover,
+                        ),
+                      ),
+                    ),
+
+
+                    Positioned(
+                        left: 1.w,
+                        right: 1.w,
+                        bottom: 16.sp,
+                        child:Text(
+                      'Beijing',
+                      textAlign: TextAlign.center,
+                      style: TextStyle(
+                        color: Colors.white,
+                        fontSize: 20.sp,
+                        fontWeight: FontWeight.w500,
+                      ),
+                    ))
+                  ],
+                );
+              }),
+            ),
+
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 1 - 0
lib/module/more/more_controller.dart

@@ -2,4 +2,5 @@ import 'package:clean/base/base_controller.dart';
 
 class MoreController extends BaseController {
 
+
 }

+ 112 - 0
lib/module/people_photo/people_photo_controller.dart

@@ -0,0 +1,112 @@
+import 'dart:io';
+
+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;
+
+class PeoplePhotoController extends GetxController {
+  final RxList<PhotoGroup> photoGroups = <PhotoGroup>[].obs;
+
+  static final Map<String, List<bool>> _savedSelections = {};
+  static bool _hasInitializedSelections = false;
+
+  int get selectedFileCount =>
+      photoGroups.fold(0, (sum, group) => sum + group.selectedCount);
+
+  double get selectedFilesSize {
+    double totalSize = 0;
+    for (var group in photoGroups) {
+      for (int i = 0; i < group.images.length; i++) {
+        if (group.selectedImages[i]) {
+          totalSize += File(group.images[i]).lengthSync();
+        }
+      }
+    }
+    return totalSize / 1024; // Convert to KB
+  }
+
+  void toggleImageSelection(String groupTitle, int imageIndex) {
+    final group = photoGroups.firstWhere((g) => g.title == groupTitle);
+    group.selectedImages[imageIndex] = !group.selectedImages[imageIndex];
+    group.isSelected.value = group.selectedImages.every((selected) => selected);
+    _saveSelections(); // 保存选择状态
+  }
+
+  void toggleGroupSelection(String groupTitle) {
+    final group = photoGroups.firstWhere((g) => g.title == groupTitle);
+    final newValue = !group.isSelected.value;
+    group.toggleSelectAll(newValue);
+    _saveSelections(); // 保存选择状态
+  }
+
+  @override
+  void onInit() {
+    super.onInit();
+    loadPhotosFromDirectory().then((_) {
+      // 恢复保存的选择状态
+      if (_hasInitializedSelections) {
+        _restoreSelections();
+      }
+    });
+  }
+
+  void _saveSelections() {
+    for (var group in photoGroups) {
+      _savedSelections[group.title] = group.selectedImages.toList();
+    }
+    _hasInitializedSelections = true;
+  }
+
+
+
+
+  void _restoreSelections() {
+    for (var group in photoGroups) {
+      // 这里假设每次页面加载时,已选择的状态会保存在 `RxList<bool>` 中。
+      group.isSelected.value = group.selectedImages.every((selected) => selected);
+    }
+  }
+  Future<void> loadPhotosFromDirectory() async {
+    try {
+      final Directory appDir = await getApplicationDocumentsDirectory();
+      final String photosPath = '${appDir.path}/photos';
+
+      final Directory photosDir = Directory(photosPath);
+      if (!await photosDir.exists()) {
+        return;
+      }
+      // 获取photos目录下的所有文件夹
+      final List<Directory> subDirs = await photosDir
+          .list()
+          .where((entity) => entity is Directory)
+          .map((e) => e as Directory)
+          .toList();
+
+      // 遍历每个文件夹
+      for (final dir in subDirs) {
+        // 获取文件夹中的所有图片
+        final List<FileSystemEntity> files = await dir
+            .list()
+            .where((entity) =>
+        entity is File &&
+            ['.jpg', '.jpeg', '.png']
+                .contains(p.extension(entity.path).toLowerCase()))
+            .toList();
+
+        if (files.isNotEmpty) {
+          // 为每个文件夹创建一个PhotoGroup
+          photoGroups.add(PhotoGroup(
+            // title: '${p.basename(dir.path)}: ${files.length}', // 使用文件夹名作为标题
+            title: 'people : ${files.length}', // 使用文件夹名作为标题
+            imageCount: files.length,
+            isSelected: false,
+            images: files.map((file) => file.path).toList(),
+          ));
+        }
+      }
+    } catch (e) {
+      print('Error loading photos: $e');
+    }
+  }
+}

+ 293 - 0
lib/module/people_photo/people_photo_view.dart

@@ -0,0 +1,293 @@
+import 'dart:io';
+
+import 'package:clean/base/base_page.dart';
+import 'package:clean/module/people_photo/people_photo_controller.dart';
+import 'package:clean/resource/assets.gen.dart';
+import 'package:clean/router/app_pages.dart';
+import 'package:flutter/Material.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+
+class PeoplePhotoPage extends BasePage<PeoplePhotoController> {
+  const PeoplePhotoPage({super.key});
+
+  static start() {
+    Get.put((PeoplePhotoController()));
+    Get.toNamed(RoutePath.peoplePhoto, arguments: {});
+  }
+
+  @override
+  bool statusBarDarkFont() {
+    // TODO: implement statusBarDarkFont
+    return false;
+  }
+
+  @override
+  bool immersive() {
+    // TODO: implement immersive
+    return true;
+  }
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Stack(children: [
+      Container(
+        child: SafeArea(
+          child: Column(
+            children: [
+              _titleCard(),
+              // Photo groups
+              Expanded(
+                child: Obx(() {
+                  return ListView(
+                    padding: EdgeInsets.symmetric(horizontal: 16.w),
+                    children: [
+                      ...controller.photoGroups.map((group) => Column(
+                            children: [
+                              _buildPhotoGroup(
+                                title: group.title,
+                                imageCount: group.imageCount,
+                              ),
+                              SizedBox(height: 15.h),
+                            ],
+                          ))
+                    ],
+                  );
+                }),
+              ),
+              _bottomBarCard(),
+            ],
+          ),
+        ),
+      ),
+      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),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          GestureDetector(
+            onTap: () => Get.back(),
+            child: Assets.images.iconBackArrow.image(
+              width: 28.w,
+              height: 28.h,
+            ),
+          ),
+          SizedBox(height: 12.h),
+          Text(
+            'People Photos',
+            style: TextStyle(
+              color: Colors.white,
+              fontSize: 24.sp,
+              fontWeight: FontWeight.w700,
+            ),
+          ),
+          SizedBox(height: 20.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(() => Text(
+                '${controller.selectedFileCount} files selected (${controller.selectedFilesSize.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,
+                  ),
+                ),
+
+                Assets.images.iconDelete.image(
+                  width: 18.w,
+                  height: 18.h,
+                ),
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildPhotoGroup({
+    required String title,
+    required int imageCount,
+  }) {
+    return Container(
+      padding: EdgeInsets.symmetric(horizontal: 12.w),
+      margin: EdgeInsets.only(top: 14.h),
+      width: 328.w,
+      height: 230.h,
+      decoration: ShapeDecoration(
+        color: Colors.white.withValues(alpha: 0.12),
+        shape: RoundedRectangleBorder(
+          borderRadius: BorderRadius.circular(14.sp),
+        ),
+      ),
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+        children: [
+          Row(
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            children: [
+              Text(
+                title,
+                textAlign: TextAlign.center,
+                style: TextStyle(
+                  color: Colors.white,
+                  fontSize: 14.sp,
+                  fontWeight: FontWeight.w500,
+                ),
+              ),
+              GestureDetector(
+                onTap: () => controller.toggleGroupSelection(title),
+                child: Obx(() => Text(
+                      controller.photoGroups
+                              .firstWhere((g) => g.title == title)
+                              .isSelected
+                              .value
+                          ? 'Deselect All'
+                          : 'Select All',
+                      style: TextStyle(
+                        color: Colors.white.withValues(alpha: 0.7),
+                        fontSize: 14.sp,
+                        fontWeight: FontWeight.w400,
+                      ),
+                    )),
+              ),
+            ],
+          ),
+          SizedBox(
+            height: imageCount <= 8 ? null : 148.w,
+            child: imageCount <= 8
+                ? GridView.builder(
+                    shrinkWrap: true,
+                    physics: NeverScrollableScrollPhysics(),
+                    gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+                      crossAxisCount: 4,
+                      mainAxisSpacing: 8.w,
+                      crossAxisSpacing: 8.h,
+                    ),
+                    itemCount: imageCount,
+                    itemBuilder: _buildPhotoItem(title),
+                  )
+                : GridView.builder(
+                    scrollDirection: Axis.horizontal,
+                    physics: BouncingScrollPhysics(),
+                    gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+                      crossAxisCount: 2,
+                      mainAxisSpacing: 8.w,
+                      crossAxisSpacing: 8.h,
+                      childAspectRatio: 1,
+                    ),
+                    itemCount: imageCount,
+                    itemBuilder: _buildPhotoItem(title),
+                  ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  Widget Function(BuildContext, int) _buildPhotoItem(String title) =>
+      (context, index) {
+        final group =
+            controller.photoGroups.firstWhere((group) => group.title == title);
+        final imagePath = group.images[index];
+
+        return GestureDetector(
+          onTap: () => controller.toggleImageSelection(title, index),
+          child: Obx(() {
+            final isSelected = group.selectedImages[index];
+            return Stack(
+              children: [
+                Container(
+                  width: 70.w,
+                  height: 70.w,
+                  decoration: ShapeDecoration(
+                    color: Colors.white.withValues(alpha: 0.12),
+                    shape: RoundedRectangleBorder(
+                      borderRadius: BorderRadius.circular(9.27.sp),
+                    ),
+                    image: DecorationImage(
+                      image: FileImage(File(imagePath)),
+                      fit: BoxFit.cover,
+                    ),
+                  ),
+                ),
+                Positioned(
+                  right: 6.w,
+                  bottom: 6.h,
+                  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,
+                          )),
+                  ),
+                ),
+              ],
+            );
+          }),
+        );
+      };
+}

+ 23 - 0
lib/module/people_photo/photo_group.dart

@@ -0,0 +1,23 @@
+import 'package:get/get.dart';
+class PhotoGroup {
+  final String title;
+  final int imageCount;
+  final RxBool isSelected;
+  final List<String> images;
+  final RxList<bool> selectedImages;
+
+  int get selectedCount => selectedImages.where((selected) => selected).length;
+
+  PhotoGroup({
+    required this.title,
+    required this.imageCount,
+    required bool isSelected,
+    required this.images,
+  })  : isSelected = isSelected.obs,
+        selectedImages = List.generate(imageCount, (_) => isSelected).obs;
+
+  void toggleSelectAll(bool value) {
+    isSelected.value = value;
+    selectedImages.assignAll(List.generate(images.length, (_) => value));
+  }
+}

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

@@ -0,0 +1,128 @@
+import 'dart:io';
+
+import 'package:get/get.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:path/path.dart' as p;
+
+class PhotoGroup {
+  final String title;
+  final int imageCount;
+  final RxBool isSelected;
+  final List<String> images;
+  final RxList<bool> selectedImages;
+
+  int get selectedCount => selectedImages.where((selected) => selected).length;
+
+  PhotoGroup({
+    required this.title,
+    required this.imageCount,
+    required bool isSelected,
+    required this.images,
+  })  : isSelected = isSelected.obs,
+        selectedImages = List.generate(imageCount, (_) => isSelected).obs;
+
+  void toggleSelectAll(bool value) {
+    isSelected.value = value;
+    selectedImages.assignAll(List.generate(images.length, (_) => value));
+  }
+}
+
+class SimilarPhotoController extends GetxController {
+  final RxList<PhotoGroup> photoGroups = <PhotoGroup>[].obs;
+
+  static final Map<String, List<bool>> _savedSelections = {};
+  static bool _hasInitializedSelections = false;
+
+  int get selectedFileCount =>
+      photoGroups.fold(0, (sum, group) => sum + group.selectedCount);
+
+  double get selectedFilesSize {
+    double totalSize = 0;
+    for (var group in photoGroups) {
+      for (int i = 0; i < group.images.length; i++) {
+        if (group.selectedImages[i]) {
+          totalSize += File(group.images[i]).lengthSync();
+        }
+      }
+    }
+    return totalSize / 1024; // Convert to KB
+  }
+
+  void toggleImageSelection(String groupTitle, int imageIndex) {
+    final group = photoGroups.firstWhere((g) => g.title == groupTitle);
+    group.selectedImages[imageIndex] = !group.selectedImages[imageIndex];
+    group.isSelected.value = group.selectedImages.every((selected) => selected);
+    _saveSelections();
+  }
+
+  void toggleGroupSelection(String groupTitle) {
+    final group = photoGroups.firstWhere((g) => g.title == groupTitle);
+    final newValue = !group.isSelected.value;
+    group.toggleSelectAll(newValue);
+    _saveSelections();
+  }
+
+  void _saveSelections() {
+    for (var group in photoGroups) {
+      _savedSelections[group.title] = group.selectedImages.toList();
+    }
+    _hasInitializedSelections = true;
+  }
+
+  void _restoreSelections() {
+    for (var group in photoGroups) {
+      // 这里假设每次页面加载时,已选择的状态会保存在 `RxList<bool>` 中。
+      group.isSelected.value =
+          group.selectedImages.every((selected) => selected);
+    }
+  }
+
+  @override
+  void onInit() {
+    super.onInit();
+    loadPhotosFromDirectory().then((_) {
+      if (_hasInitializedSelections) {
+        _restoreSelections();
+      }
+    });
+  }
+
+  Future<void> loadPhotosFromDirectory() async {
+    try {
+      final Directory appDir = await getApplicationDocumentsDirectory();
+      final String photosPath = '${appDir.path}/photos';
+
+      final Directory photosDir = Directory(photosPath);
+      if (!await photosDir.exists()) {
+        return;
+      }
+
+      final List<Directory> subDirs = await photosDir
+          .list()
+          .where((entity) => entity is Directory)
+          .map((e) => e as Directory)
+          .toList();
+
+      for (final dir in subDirs) {
+        final List<FileSystemEntity> files = await dir
+            .list()
+            .where((entity) =>
+                entity is File &&
+                ['.jpg', '.jpeg', '.png']
+                    .contains(p.extension(entity.path).toLowerCase()))
+            .toList();
+
+        if (files.isNotEmpty) {
+          photoGroups.add(PhotoGroup(
+            title: 'photo: ${files.length}',
+            imageCount: files.length,
+            isSelected: false,
+            images: files.map((file) => file.path).toList(),
+          ));
+        }
+      }
+    } catch (e) {
+      print('Error loading photos: $e');
+    }
+  }
+}

+ 405 - 0
lib/module/similar_photo/similar_photo_view.dart

@@ -0,0 +1,405 @@
+import 'dart:io';
+import 'dart:math';
+import 'package:clean/base/base_page.dart';
+import 'package:clean/module/similar_photo/similar_photo_controller.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';
+
+class SimilarPhotoPage extends BasePage<SimilarPhotoController> {
+  const SimilarPhotoPage({super.key});
+
+  static void start() {
+    Get.put((SimilarPhotoController()));
+    Get.toNamed(RoutePath.similarPhoto);
+  }
+
+  @override
+  bool statusBarDarkFont() => false;
+
+  @override
+  bool immersive() => true;
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Stack(children: [
+      Container(
+        child: SafeArea(
+          child: Column(
+            children: [
+              _titleCard(),
+              Expanded(
+                child: Obx(() {
+                  return ListView(
+                    padding: EdgeInsets.symmetric(horizontal: 16.w),
+                    children: [
+                      ...controller.photoGroups.map((group) => Column(
+                            children: [
+                              _buildPhotoGroup(
+                                title: group.title,
+                                imageCount: group.imageCount,
+
+                              ),
+                              SizedBox(height: 15.h),
+                            ],
+                          ))
+                    ],
+                  );
+                }),
+              ),
+              _bottomBarCard(),
+            ],
+          ),
+        ),
+      ),
+      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),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          GestureDetector(
+            onTap: () => Get.back(),
+            child: Assets.images.iconBackArrow.image(
+              width: 28.w,
+              height: 28.h,
+            ),
+          ),
+          SizedBox(height: 12.h),
+          Text(
+            'Similar Photos',
+            style: TextStyle(
+              color: Colors.white,
+              fontSize: 24.sp,
+              fontWeight: FontWeight.w700,
+            ),
+          ),
+          SizedBox(height: 20.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.withOpacity(0.1)),
+          borderRadius: BorderRadius.only(
+            topLeft: Radius.circular(14.r),
+            topRight: Radius.circular(14.r),
+          ),
+        ),
+      ),
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: [
+          Obx(() => Text(
+                '${controller.selectedFileCount} files selected (${controller.selectedFilesSize.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,
+                  ),
+                ),
+                Assets.images.iconDelete.image(
+                  width: 18.w,
+                  height: 18.h,
+                ),
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildPhotoGroup({
+    required String title,
+    required int imageCount,
+
+  }) {
+    return Container(
+      padding: EdgeInsets.symmetric(horizontal: 12.w),
+      margin: EdgeInsets.only(top: 14.h),
+      width: 328.w,
+      height: 258.h,
+      decoration: ShapeDecoration(
+        color: Colors.white.withValues(alpha: 0.12),
+        shape: RoundedRectangleBorder(
+          borderRadius: BorderRadius.circular(14.sp),
+        ),
+      ),
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+        children: [
+          Row(
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            children: [
+              Row(
+                children: [
+                  Text(
+                    title,
+                    textAlign: TextAlign.center,
+                    style: TextStyle(
+                      color: Colors.white,
+                      fontSize: 14.sp,
+                      fontWeight: FontWeight.w500,
+                    ),
+                  ),
+
+                ],
+              ),
+              GestureDetector(
+                onTap: () => controller.toggleGroupSelection(title),
+                child: Obx(() => Text(
+                      controller.photoGroups
+                              .firstWhere((g) => g.title == title)
+                              .isSelected
+                              .value
+                          ? 'Deselect All'
+                          : 'Select All',
+                      style: TextStyle(
+                        color: Colors.white.withValues(alpha: 0.7),
+                        fontSize: 14.sp,
+                        fontWeight: FontWeight.w400,
+                      ),
+                    )),
+              ),
+            ],
+          ),
+          SizedBox(
+            height: 148.h,
+            child: ListView(
+              scrollDirection: Axis.horizontal,
+              physics: BouncingScrollPhysics(),
+              children: [
+                // 第一张大图
+                if (imageCount > 0)
+                  GestureDetector(
+                    onTap: () => controller.toggleImageSelection(title, 0),
+                    child: SizedBox(
+                      width: 148.w,
+                      height: 148.h,
+                      child: Obx(() {
+                        final group = controller.photoGroups
+                            .firstWhere((g) => g.title == title);
+                        return Stack(
+                          children: [
+                            Container(
+                              decoration: ShapeDecoration(
+                                color: Colors.white.withValues(alpha: 0.12),
+                                shape: RoundedRectangleBorder(
+                                  borderRadius: BorderRadius.circular(8.r),
+                                ),
+                                image: DecorationImage(
+                                  image: FileImage(File(group.images[0])),
+                                  fit: BoxFit.cover,
+                                ),
+                              ),
+                            ),
+                            Positioned(
+                              left: 8.w,
+                              top: 8.h,
+                              child: Container(
+
+                                width: 108.w,
+                                height: 26.h,
+                                padding: EdgeInsets.symmetric(
+                                  horizontal: 8.w,
+                                  vertical: 4.h,
+                                ),
+                                decoration: ShapeDecoration(
+                                  color: Colors.black.withValues(alpha: 0.74),
+                                  shape: RoundedRectangleBorder(
+                                    borderRadius:
+                                        BorderRadius.circular(14.21.r),
+                                  ),
+                                ),
+                                child: Row(
+                                  mainAxisAlignment:
+                                      MainAxisAlignment.spaceAround,
+                                  children: [
+                                    Assets.images.iconSimilarBest.image(
+                                      width: 11.37.w,
+                                      height: 11.37.h,
+                                    ),
+                                    Text(
+                                      'Best result',
+                                      textAlign: TextAlign.center,
+                                      style: TextStyle(
+                                        color: Colors.white,
+                                        fontSize: 13.sp,
+                                        fontWeight: FontWeight.w400,
+                                      ),
+                                    ),
+                                  ],
+                                ),
+                              ),
+                            ),
+                            Positioned(
+                              right: 4.w,
+                              bottom: 4.h,
+                              child: Container(
+                                child: group.selectedImages[0]
+                                    ? Center(
+                                        child: Assets.images.iconSelected.image(
+                                          width: 16.w,
+                                          height: 16.h,
+                                        ),
+                                      )
+                                    : Center(
+                                        child:
+                                            Assets.images.iconUnselected.image(
+                                          width: 16.w,
+                                          height: 16.h,
+                                        ),
+                                      ),
+                              ),
+                            ),
+                          ],
+                        );
+                      }),
+                    ),
+                  ),
+                // 其他图片2x2网格
+                if (imageCount > 1)
+                  ...List.generate(((imageCount - 1) / 4).ceil(), (gridIndex) {
+                    return Container(
+                      margin: EdgeInsets.only(left: 8.w),
+                      width: 142.w,
+                      child: GridView.count(
+                        physics: NeverScrollableScrollPhysics(),
+                        crossAxisCount: 2,
+                        mainAxisSpacing: 8.h,
+                        crossAxisSpacing: 8.w,
+                        children: List.generate(
+                          min(4, imageCount - 1 - gridIndex * 4),
+                          (index) {
+                            final realIndex = gridIndex * 4 + index + 1;
+                            return GestureDetector(
+                              onTap: () => controller.toggleImageSelection(
+                                  title, realIndex),
+                              child: Obx(() {
+                                final group = controller.photoGroups
+                                    .firstWhere((g) => g.title == title);
+                                return Container(
+                                  decoration: ShapeDecoration(
+                                    color: Colors.white.withOpacity(0.12),
+                                    shape: RoundedRectangleBorder(
+                                      borderRadius: BorderRadius.circular(8.r),
+                                    ),
+                                    image: DecorationImage(
+                                      image: FileImage(
+                                          File(group.images[realIndex])),
+                                      fit: BoxFit.cover,
+                                    ),
+                                  ),
+                                  child: Stack(
+                                    children: [
+                                      Positioned(
+                                        right: 4.w,
+                                        bottom: 4.h,
+                                        child: Obx(() {
+                                          final isSelected =
+                                              group.selectedImages[realIndex];
+                                          return Container(
+                                            child: isSelected
+                                                ? Center(
+                                                    child: Assets
+                                                        .images.iconSelected
+                                                        .image(
+                                                      width: 16.w,
+                                                      height: 16.h,
+                                                    ),
+                                                  )
+                                                : Center(
+                                                    child: Assets
+                                                        .images.iconUnselected
+                                                        .image(
+                                                      width: 16.w,
+                                                      height: 16.h,
+                                                    ),
+                                                  ),
+                                          );
+                                        }),
+                                      ),
+                                    ],
+                                  ),
+                                );
+                              }),
+                            );
+                          },
+                        ),
+                      ),
+                    );
+                  }),
+              ],
+            ),
+          ),
+          Container(
+
+            width: 162.w,
+            height: 38.h,
+            decoration: ShapeDecoration(
+              color: Color(0xFF0279FB),
+              shape: RoundedRectangleBorder(
+                borderRadius: BorderRadius.circular(10.r),
+              ),
+            ),
+
+
+            child: Center(
+              child: Obx(() => Text(
+                    'Move ${controller.photoGroups.firstWhere((g) => g.title == title).selectedCount} to trash',
+                    style: TextStyle(
+                      color: Colors.white,
+                      fontSize: 16.sp,
+                      fontWeight: FontWeight.w500,
+                    ),
+                  )),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 16 - 0
lib/router/app_pages.dart

@@ -1,5 +1,11 @@
 import 'package:clean/module/home/home_controller.dart';
+import 'package:clean/module/locations_photo/locations_photo_controller.dart';
+import 'package:clean/module/locations_photo/locations_photo_view.dart';
 import 'package:clean/module/main/main_view.dart';
+import 'package:clean/module/people_photo/people_photo_controller.dart';
+import 'package:clean/module/people_photo/people_photo_view.dart';
+import 'package:clean/module/similar_photo/similar_photo_controller.dart';
+import 'package:clean/module/similar_photo/similar_photo_view.dart';
 import 'package:get/get.dart';
 import 'package:get/get_core/src/get_main.dart';
 import 'package:get/get_instance/src/bindings_interface.dart';
@@ -14,6 +20,9 @@ abstract class AppPage {
 
 abstract class RoutePath {
   static const mainTab = '/mainTab';
+  static const peoplePhoto = '/peoplePhoto';
+  static const similarPhoto = '/similarPhoto';
+  static const locationsPhoto = '/locationsPhoto';
 }
 
 class AppBinding extends Bindings {
@@ -21,6 +30,10 @@ class AppBinding extends Bindings {
   void dependencies() {
     lazyPut(() => MainController());
     lazyPut(() => HomeController());
+    lazyPut(() => PeoplePhotoController());
+    lazyPut(() => SimilarPhotoController());
+    lazyPut(() => LocationsPhotoController());
+
   }
 
   void lazyPut<S>(InstanceBuilderCallback<S> builder) {
@@ -30,4 +43,7 @@ class AppBinding extends Bindings {
 
 final generalPages = [
   GetPage(name: RoutePath.mainTab, page: () => MainTabPage()),
+  GetPage(name: RoutePath.peoplePhoto, page: () => PeoplePhotoPage()),
+  GetPage(name: RoutePath.similarPhoto, page: () => SimilarPhotoPage()),
+  GetPage(name: RoutePath.locationsPhoto, page: () => LocationsPhotoPage()),
 ];

+ 5 - 0
pubspec.yaml

@@ -68,6 +68,11 @@ dependencies:
   # Use with the CupertinoIcons class for iOS style icons.
   cupertino_icons: ^1.0.8
 
+  #  跨平台路径操作库。
+  path: ^1.9.0
+  #  路径提供程序
+  path_provider: ^2.1.5
+
 dev_dependencies:
   flutter_test:
     sdk: flutter