Ver Fonte

[feat]新增获取全部联系人页面

Destiny há 1 ano atrás
pai
commit
bc69f9ad6c

BIN
assets/images/icon_contact_all.webp


BIN
assets/images/icon_contact_backup.webp


BIN
assets/images/icon_contact_duplicate.webp


BIN
assets/images/icon_contact_incomplete.webp


BIN
assets/images/icon_contact_main.webp


+ 3 - 0
ios/Podfile

@@ -56,6 +56,9 @@ target.build_configurations.each do |config|
         ## dart: PermissionGroup.photos
         'PERMISSION_PHOTOS=1',
 
+	## dart: PermissionGroup.contacts
+        'PERMISSION_CONTACTS=1',
+
       ]
     end
   end

+ 7 - 1
ios/Podfile.lock

@@ -12,6 +12,8 @@ PODS:
   - disk_space (0.0.1):
     - Flutter
   - Flutter (1.0.0)
+  - flutter_contacts (0.0.1):
+    - Flutter
   - image_gallery_saver (2.0.2):
     - Flutter
   - in_app_purchase_storekit (0.0.1):
@@ -53,6 +55,7 @@ DEPENDENCIES:
   - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
   - disk_space (from `.symlinks/plugins/disk_space/ios`)
   - Flutter (from `Flutter`)
+  - flutter_contacts (from `.symlinks/plugins/flutter_contacts/ios`)
   - image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`)
   - in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
   - mmkv_ios (from `.symlinks/plugins/mmkv_ios/ios`)
@@ -85,6 +88,8 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/disk_space/ios"
   Flutter:
     :path: Flutter
+  flutter_contacts:
+    :path: ".symlinks/plugins/flutter_contacts/ios"
   image_gallery_saver:
     :path: ".symlinks/plugins/image_gallery_saver/ios"
   in_app_purchase_storekit:
@@ -116,6 +121,7 @@ SPEC CHECKSUMS:
   device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
   disk_space: e94d34bbdf77954adfb39e60bde9cc5c7233eda6
   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
+  flutter_contacts: edb1c5ce76aa433e20e6cb14c615f4c0b66e0983
   image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
   in_app_purchase_storekit: 8c3b0b3eb1b0f04efbff401c3de6266d4258d433
   MMKV: 5854d45476fc3757bacfa7e13cc0fbcd274ab0e4
@@ -130,6 +136,6 @@ SPEC CHECKSUMS:
   video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
   webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4
 
-PODFILE CHECKSUM: 9cb569fbad751252ed253ec4acef25285b9cb2bd
+PODFILE CHECKSUM: 5e1afdb9869de7eeb6b0abacc114654ae68d56ef
 
 COCOAPODS: 1.16.2

+ 5 - 3
ios/Runner/Info.plist

@@ -2,6 +2,8 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
+	<key>NSContactsUsageDescription</key>
+	<string>联系人权限</string>
 	<key>CADisableMinimumFrameDurationOnPhone</key>
 	<true/>
 	<key>CFBundleDevelopmentRegion</key>
@@ -34,11 +36,11 @@
 		<true/>
 	</dict>
 	<key>NSCameraUsageDescription</key>
-	<string>"CleanPro" would like to access your camera. The app will access and store the photos and videos you take.</string>
+	<string>&quot;CleanPro&quot; would like to access your camera. The app will access and store the photos and videos you take.</string>
 	<key>NSPhotoLibraryUsageDescription</key>
-	<string>"CleanPro" would like to access your photo gallery to search for similar and duplicate photos. Access permission is required: Your photos will not be stored or used on any of our servers, nor will they be shared with third parties.</string>
+	<string>&quot;CleanPro&quot; would like to access your photo gallery to search for similar and duplicate photos. Access permission is required: Your photos will not be stored or used on any of our servers, nor will they be shared with third parties.</string>
 	<key>NSUserTrackingUsageDescription</key>
-	<string>Allow "CleanPro" to track your activities in other companies' apps and websites? This data will be used for analytics purposes. We do not collect your personal data. If you decline, tracking will not occur.</string>
+	<string>Allow &quot;CleanPro&quot; to track your activities in other companies&apos; apps and websites? This data will be used for analytics purposes. We do not collect your personal data. If you decline, tracking will not occur.</string>
 	<key>UIApplicationSupportsIndirectInputEvents</key>
 	<true/>
 	<key>UILaunchStoryboardName</key>

+ 80 - 0
lib/module/contact/all/all_controller.dart

@@ -0,0 +1,80 @@
+import 'package:clean/base/base_controller.dart';
+import 'package:clean/module/contact/contact_state.dart';
+import 'package:flutter/Material.dart';
+import 'package:flutter_contacts/contact.dart';
+import 'package:get/get.dart';
+import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
+
+class AllController extends BaseController {
+  // 是否为编辑状态
+  RxBool isEdit = false.obs;
+
+  // 是否全选
+  RxBool isAllSelected = false.obs;
+
+  // 存储选中的图片ID
+  final RxSet<String> selectedContacts = <String>{}.obs;
+
+  final ItemScrollController itemScrollController = ItemScrollController();
+  final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create();
+
+  @override
+  void onInit() {
+    // TODO: implement onInit
+    super.onInit();
+
+  }
+
+  void scrollToInitial(String initial) {
+    int index = ContactState.initials.indexOf(initial);
+    if (index != -1) {
+      itemScrollController.jumpTo(
+        index: index,
+        // curve: Curves,
+      );
+    }
+  }
+
+  // 选择/取消选择联系人
+  void toggleSelectContact(Contact selectContact) {
+    final asset = ContactState.contactList.firstWhere((contact) => contact.id == selectContact.id);
+
+    if (selectedContacts.contains(selectContact.id)) {
+      selectedContacts.remove(selectContact.id);
+    } else {
+      selectedContacts.add(selectContact.id);
+    }
+    // 更新全选状态
+    isAllSelected.value = ContactState.selectedContact.length == ContactState.contactList.length;
+  }
+
+  // 全选/取消全选
+    void toggleSelectAll() {
+    if (isAllSelected.value) {
+      selectedContacts.clear();
+    } else {
+      selectedContacts.addAll(ContactState.contactList.map((contact) => contact.id));
+    }
+    isAllSelected.value = !isAllSelected.value;
+  }
+
+  // 退出编辑模式时清空选择
+  void exitEditMode() {
+    isEdit.value = false;
+    selectedContacts.clear();
+    isAllSelected.value = false;
+  }
+
+  void deleteBtnClick() {
+    // 获取要删除的资产
+    final contactToDelete =
+    ContactState.contactList.where((contact) => selectedContacts.contains(contact.id)).toList();
+
+    for (var contact in contactToDelete) {
+      contact.delete();
+    }
+
+    exitEditMode();
+    ContactState.loadContacts();
+  }
+}

+ 344 - 0
lib/module/contact/all/all_view.dart

@@ -0,0 +1,344 @@
+import 'package:clean/base/base_page.dart';
+import 'package:clean/module/contact/all/all_controller.dart';
+import 'package:clean/module/contact/contact_state.dart';
+import 'package:clean/resource/assets.gen.dart';
+import 'package:clean/utils/expand.dart';
+import 'package:flutter/Material.dart';
+import 'package:flutter_contacts/flutter_contacts.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
+
+class AllPage extends BasePage<AllController> {
+  const AllPage({super.key});
+
+  @override
+  bool immersive() {
+    return true;
+  }
+
+  @override
+  bool statusBarDarkFont() => false;
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Stack(
+      children: [
+        buildMain(context),
+        IgnorePointer(
+          child: Assets.images.bgHome.image(
+            width: 360.w,
+          ),
+        ),
+      ],
+    );
+  }
+
+  Widget buildMain(BuildContext context) {
+    return SafeArea(
+      child: Container(
+        padding: EdgeInsets.only(left: 16.w, top: 14.h, right: 16.w),
+        child: Obx(() {
+          return Column(
+            mainAxisAlignment: MainAxisAlignment.start,
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              !controller.isEdit.value
+                  ? Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                children: [
+                  GestureDetector(
+                    onTap: () {
+                      Get.back();
+                    },
+                    child: Assets.images.iconCommonBack
+                        .image(width: 28.w, height: 28.w),
+                  ),
+                  GestureDetector(
+                    onTap: () {
+                      controller.isEdit.value = true;
+                    },
+                    child: Container(
+                      width: 71.w,
+                      height: 30.h,
+                      decoration: BoxDecoration(
+                        color: "#1F2D3F".color,
+                        borderRadius: BorderRadius.all(
+                          Radius.circular(15.h),
+                        ),
+                      ),
+                      child: Center(
+                        child: Text(
+                          "Select",
+                          style: TextStyle(
+                            color: Colors.white,
+                            fontSize: 14.sp,
+                          ),
+                        ),
+                      ),
+                    ),
+                  ),
+                ],
+              )
+                  : Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                children: [
+                  GestureDetector(
+                    onTap: () {
+                      controller.isEdit.value = false;
+                    },
+                    child: Container(
+                      width: 71.w,
+                      height: 30.h,
+                      decoration: BoxDecoration(
+                        color: "#1F2D3F".color,
+                        borderRadius: BorderRadius.all(
+                          Radius.circular(15.h),
+                        ),
+                      ),
+                      child: Center(
+                        child: Text(
+                          "Cancel",
+                          style: TextStyle(
+                            color: Colors.white,
+                            fontSize: 14.sp,
+                          ),
+                        ),
+                      ),
+                    ),
+                  ),
+                  GestureDetector(
+                    onTap: () {
+                      controller.toggleSelectAll();
+                    },
+                    child: Text(
+                      controller.isAllSelected.value
+                          ? "Deselect all"
+                          : "Select All",
+                      style: TextStyle(
+                        color: Colors.white.withOpacity(0.65),
+                        fontSize: 14.sp,
+                      ),
+                    ),
+                  ),
+                ],
+              ),
+              SizedBox(
+                height: 12.h,
+              ),
+              Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                children: [
+                  Text(
+                    "All Contacts",
+                    style: TextStyle(
+                      color: Colors.white,
+                      fontWeight: FontWeight.w700,
+                      fontSize: 24.sp,
+                    ),
+                  ),
+                ],
+              ),
+              Expanded(
+                child: Row(
+                  children: [
+                    Expanded(
+                      child: Obx(() {
+                        return ScrollablePositionedList.builder(
+                          itemCount: ContactState.initials.length,
+                          itemScrollController: controller.itemScrollController,
+                          itemPositionsListener: controller
+                              .itemPositionsListener,
+                          itemBuilder: (context, index) {
+                            String initial = ContactState.initials[index];
+                            var groupedContacts =
+                            ContactState.groupedContacts[initial];
+                            return Column(
+                              crossAxisAlignment: CrossAxisAlignment.start,
+                              children: [
+                                SizedBox(
+                                  height: 12.h,
+                                ),
+                                Padding(
+                                  padding: EdgeInsets.symmetric(vertical: 8.h),
+                                  child: Text(
+                                    initial,
+                                    style: TextStyle(
+                                      color: Colors.white.withOpacity(0.7),
+                                      fontSize: 14.sp,
+                                      fontWeight: FontWeight.w500,
+                                    ),
+                                  ),
+                                ),
+                                ...ContactState.groupedContacts[initial]!
+                                    .asMap()
+                                    .entries
+                                    .map((entry) {
+                                  int index = entry.key; // 当前联系人的索引
+                                  Contact contact = entry.value; // 当前联系人
+                                  bool isFirst = index == 0; // 是否是第一个
+                                  bool isLast = index ==
+                                      (ContactState.groupedContacts[initial]
+                                          ?.length ??
+                                          0) -
+                                          1; // 是否是最后一个
+                                  return Container(
+                                    padding: EdgeInsets.all(10.w),
+                                    width: double.infinity,
+                                    // height: 62.h,
+                                    decoration: BoxDecoration(
+                                      borderRadius: BorderRadius.vertical(
+                                        top: isFirst
+                                            ? Radius.circular(12)
+                                            : Radius.zero, // 第一个设置上圆角
+                                        bottom: isLast
+                                            ? Radius.circular(12)
+                                            : Radius.zero, // 最后一个设置下圆角
+                                      ),
+                                      color: Colors.white.withOpacity(0.12),
+                                    ),
+                                    child: Row(
+                                      mainAxisAlignment:
+                                      MainAxisAlignment.spaceBetween,
+                                      children: [
+                                        Column(
+                                          mainAxisAlignment:
+                                          MainAxisAlignment.start,
+                                          crossAxisAlignment:
+                                          CrossAxisAlignment.start,
+                                          children: [
+                                            Text(
+                                              contact.displayName ?? '未命名',
+                                              style: TextStyle(
+                                                color: Colors.white,
+                                                fontSize: 14.sp,
+                                                fontWeight: FontWeight.w500,
+                                              ),
+                                            ),
+                                            SizedBox(
+                                              height: 5.h,
+                                            ),
+                                            Text(
+                                              contact.phones.first.number ??
+                                                  '无号码',
+                                              style: TextStyle(
+                                                color: Colors.white,
+                                                fontSize: 14.sp,
+                                                fontWeight: FontWeight.w500,
+                                              ),
+                                            ),
+                                          ],
+                                        ),
+                                        // 删除按钮
+                                        Visibility(
+                                          visible: controller.isEdit.value,
+                                          child: GestureDetector(
+                                            onTap: () {
+                                              controller.toggleSelectContact(
+                                                  contact);
+                                            },
+                                            child: Container(
+                                              child: controller.selectedContacts
+                                                  .contains(contact.id)
+                                                  ? Center(
+                                                child: Assets
+                                                    .images.iconSelected
+                                                    .image(
+                                                  width: 16.w,
+                                                  height: 16.h,
+                                                ),
+                                              )
+                                                  : Center(
+                                                child: Assets
+                                                    .images.iconUnselected
+                                                    .image(
+                                                  width: 16.w,
+                                                  height: 16.h,
+                                                ),
+                                              ),
+                                            ),
+                                          ),
+                                        ),
+                                      ],
+                                    ),
+                                  );
+                                }),
+                                SizedBox(
+                                  height: 12.h,
+                                ),
+                              ],
+                            );
+                          },
+                        );
+                      }),
+                    ),
+                    Container(
+                      width: 30,
+                      child: ListView.builder(
+                        // physics: NeverScrollableScrollPhysics(),
+                        itemCount: ContactState.initials.length,
+                        itemBuilder: (context, index) {
+                          return GestureDetector(
+                            onTap: () =>
+                                controller
+                                    .scrollToInitial(
+                                    ContactState.initials[index]),
+                            child: Padding(
+                              padding: EdgeInsets.symmetric(vertical: 2),
+                              child: Text(
+                                ContactState.initials[index],
+                                textAlign: TextAlign.center,
+                                style: TextStyle(
+                                  fontSize: 14,
+                                  color: "#0279FB".color,
+                                  fontWeight: FontWeight.bold,
+                                ),
+                              ),
+                            ),
+                          );
+                        },
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+              Visibility(
+                visible: controller.isEdit.value,
+                child: GestureDetector(
+                  onTap: () {
+                    controller.deleteBtnClick();
+                  },
+                  child: Container(
+                    width: 328.w,
+                    height: 48.h,
+                    decoration: BoxDecoration(
+                      color: "#0279FB".color,
+                      borderRadius: BorderRadius.all(
+                        Radius.circular(10.r),
+                      ),
+                    ),
+                    child: Center(
+                      child: Row(
+                        mainAxisAlignment: MainAxisAlignment.center,
+                        children: [
+                          Text(
+                            "Delete",
+                            style: TextStyle(
+                              color: Colors.white,
+                              fontSize: 16.sp,
+                              fontWeight: FontWeight.w500,
+                            ),
+                          ),
+                        ],
+                      ),
+                    ),
+                  ),
+                ),
+              )
+            ],
+          );
+        }),
+      ),
+    );
+  }
+}

+ 5 - 0
lib/module/contact/backup/controller.dart

@@ -0,0 +1,5 @@
+import 'package:clean/base/base_controller.dart';
+
+class ContactBackUpController extends BaseController {
+
+}

+ 31 - 0
lib/module/contact/backup/view.dart

@@ -0,0 +1,31 @@
+import 'package:clean/base/base_page.dart';
+import 'package:clean/module/contact/backup/controller.dart';
+import 'package:clean/resource/assets.gen.dart';
+import 'package:flutter/Material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+
+class ContactBackUpPage extends BasePage<ContactBackUpController> {
+  const ContactBackUpPage({super.key});
+
+  @override
+  bool immersive() {
+    return true;
+  }
+
+  @override
+  bool statusBarDarkFont() => false;
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Stack(
+      children: [
+        // buildMain(context),
+        IgnorePointer(
+          child: Assets.images.bgHome.image(
+            width: 360.w,
+          ),
+        ),
+      ],
+    );
+  }
+}

+ 83 - 0
lib/module/contact/contact_controller.dart

@@ -0,0 +1,83 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:clean/base/base_controller.dart';
+import 'package:clean/module/contact/contact_state.dart';
+import 'package:flutter_contacts/flutter_contacts.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:permission_handler/permission_handler.dart';
+
+import '../../utils/toast_util.dart';
+
+class ContactController extends BaseController {
+  @override
+  Future<void> onInit() async {
+    // TODO: implement onInit
+    super.onInit();
+
+    if (await Permission.contacts.request().isGranted) {
+      await ContactState.loadContacts();
+      // await restoreContacts();
+    } else {
+      ToastUtil.show("请先开启相册权限");
+    }
+  }
+
+  Future<void> backUpContacts() async {
+    // 获取所有联系人
+    List<Contact> contacts = await FlutterContacts.getContacts(
+      withProperties: true,
+      withPhoto: true,
+    );
+
+    // 将联系人数据转换为 JSON
+    List<Map<String, dynamic>> contactsJson =
+        contacts.map((contact) => contact.toJson()).toList();
+
+    try {
+
+      final directory = await getApplicationDocumentsDirectory();
+      final file = File('${directory.path}/contacts_backup.json');
+      await file.writeAsString(jsonEncode(contactsJson));
+
+      print('备份成功: ${file.path}');
+    } catch (e) {
+      print('备份失败: $e');
+    }
+  }
+
+  Future<void> restoreContacts() async {
+    try {
+
+      // 获取所有联系人
+      List<Contact> contacts = await FlutterContacts.getContacts(
+        withProperties: true,
+        withPhoto: true,
+      );
+
+      for (var contact in contacts) {
+        await contact.delete();
+      }
+
+      final directory = await getApplicationDocumentsDirectory();
+      // 从本地文件读取备份数据
+      final file = File('${directory.path}/contacts_backup.json');
+      String contactsJsonString = await file.readAsString();
+      List<dynamic> contactsJson = jsonDecode(contactsJsonString);
+
+      // 将 JSON 数据转换为联系人对象并写入通讯录
+      for (var contactJson in contactsJson) {
+        Contact contact = Contact.fromJson(contactJson);
+        contact.id = "";
+        await FlutterContacts.insertContact(contact);
+      }
+
+      print('恢复成功');
+    } catch (e) {
+      print('恢复失败: $e');
+    }
+  }
+
+
+  void init() {}
+}

+ 50 - 0
lib/module/contact/contact_state.dart

@@ -0,0 +1,50 @@
+import 'package:flutter_contacts/contact.dart';
+import 'package:flutter_contacts/flutter_contacts.dart';
+import 'package:get/get.dart';
+
+class ContactState {
+
+  static RxList<Contact> contactList = <Contact>[].obs;
+
+  static RxList<String> initials = <String>[].obs;
+
+  static RxMap<String, List<Contact>> groupedContacts = <String, List<Contact>>{}.obs;
+
+  static RxList<Contact> selectedContact = <Contact>[].obs;
+
+  static Future<void> loadContacts() async {
+    await getContacts();
+    ContactState.groupedContacts.value = groupContactsByInitial(ContactState.contactList);
+    ContactState.initials.value = ContactState.groupedContacts.keys.toList()..sort();
+  }
+
+  static Future<void> getContacts() async {
+
+    // 获取所有联系人
+    List<Contact> contacts = await FlutterContacts.getContacts(
+      withProperties: true,
+      withPhoto: true,
+    );
+
+    // 按名字的首字母排序
+    contacts.sort((a, b) => (a.displayName ?? '').compareTo(b.displayName ?? ''));
+    ContactState.contactList.value = contacts;
+  }
+
+  static Map<String, List<Contact>> groupContactsByInitial(List<Contact> contacts) {
+    Map<String, List<Contact>> groupedContacts = {};
+
+    for (var contact in contacts) {
+      String initial = (contact.displayName ?? '').isNotEmpty
+          ? contact.displayName[0].toUpperCase()
+          : '#';
+
+      if (!groupedContacts.containsKey(initial)) {
+        groupedContacts[initial] = [];
+      }
+      groupedContacts[initial]!.add(contact);
+    }
+
+    return groupedContacts;
+  }
+}

+ 168 - 0
lib/module/contact/contact_view.dart

@@ -0,0 +1,168 @@
+import 'package:clean/base/base_page.dart';
+import 'package:clean/module/contact/contact_controller.dart';
+import 'package:clean/module/more/more_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 ContactPage extends BasePage<ContactController> {
+  const ContactPage({super.key});
+
+  @override
+  bool immersive() {
+    return true;
+  }
+
+  @override
+  bool statusBarDarkFont() => false;
+
+  @override
+  Widget buildBody(BuildContext context) {
+    controller.init();
+    return Stack(
+      children: [
+        buildMain(context),
+        IgnorePointer(
+          child: Assets.images.bgHome.image(
+            width: 360.w,
+          ),
+        ),
+      ],
+    );
+  }
+
+  Widget buildMain(BuildContext context) {
+    return SafeArea(
+      child: Container(
+        padding: EdgeInsets.only(left: 16.w, top: 14.h, right: 16.w),
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Row(
+              mainAxisAlignment: MainAxisAlignment.spaceBetween,
+              children: [
+                GestureDetector(
+                  onTap: () {
+                    Get.back();
+                  },
+                  child: Assets.images.iconCommonBack
+                      .image(width: 28.w, height: 28.w),
+                ),
+              ],
+            ),
+            SizedBox(
+              height: 12.h,
+            ),
+            Row(
+              mainAxisAlignment: MainAxisAlignment.spaceBetween,
+              children: [
+                Text(
+                  "Manage Contacts",
+                  style: TextStyle(
+                    color: Colors.white,
+                    fontWeight: FontWeight.w700,
+                    fontSize: 24.sp,
+                  ),
+                ),
+              ],
+            ),
+            SizedBox(
+              height: 25.h,
+            ),
+            Center(
+              child: Assets.images.iconContactMain
+                  .image(width: 138.w, height: 138.w),
+            ),
+            SizedBox(
+              height: 33.h,
+            ),
+            Row(
+              mainAxisAlignment: MainAxisAlignment.spaceBetween,
+              children: [
+                _buildContactBtn(
+                  "All Contacts",
+                  Assets.images.iconContactAll.image(
+                    width: 40.w,
+                    height: 40.w,
+                  ),
+                  onTap: () {
+                    Get.toNamed(RoutePath.contactAll);
+                  },
+                ),
+                _buildContactBtn(
+                  "Duplicate",
+                  Assets.images.iconContactDuplicate.image(
+                    width: 40.w,
+                    height: 40.w,
+                  ),
+                  onTap: () {
+
+                  },
+                ),
+              ],
+            ),
+            SizedBox(
+              height: 16.h,
+            ),
+            Row(
+              mainAxisAlignment: MainAxisAlignment.spaceBetween,
+              children: [
+                _buildContactBtn(
+                  "Incomplete",
+                  Assets.images.iconContactIncomplete.image(
+                    width: 40.w,
+                    height: 40.w,
+                  ),
+                  onTap: () {
+
+                  },
+                ),
+                _buildContactBtn(
+                  "Backup",
+                  Assets.images.iconContactBackup.image(
+                    width: 40.w,
+                    height: 40.w,
+                  ),
+                  onTap: () {
+
+                  },
+                ),
+              ],
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildContactBtn(String title, Image image, {required Function() onTap}) {
+    return GestureDetector(
+      onTap: onTap,
+      child: Container(
+        width: 156.w,
+        height: 101.h,
+        decoration: BoxDecoration(
+          color: Colors.white.withOpacity(0.12),
+          borderRadius: BorderRadius.all(Radius.circular(14.r)),
+        ),
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+          children: [
+            image,
+            Text(
+              title,
+              style: TextStyle(
+                color: Colors.white.withOpacity(0.9),
+                fontSize: 16.sp,
+                fontWeight: FontWeight.w500,
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 5 - 0
lib/module/contact/duplicate/controller.dart

@@ -0,0 +1,5 @@
+import 'package:clean/base/base_controller.dart';
+
+class ContactDuplicateController extends BaseController {
+
+}

+ 0 - 0
lib/module/contact/duplicate/view.dart


+ 0 - 0
lib/module/contact/incomplete/controller.dart


+ 0 - 0
lib/module/contact/incomplete/view.dart


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

@@ -77,7 +77,7 @@ class HomeController extends BaseController {
 
       handlePhotos();
     } else {
-      ToastUtil.show("请先开启相册权限");
+      ToastUtil.show("Please enable the album permission");
     }
 
     await userRepository.getUserInfo();

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

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

+ 10 - 0
lib/module/more/more_view.dart

@@ -91,6 +91,16 @@ class MorePage extends BaseView<MoreController> {
                       },
                     ),
                     SizedBox(height: 25.h),
+                    _buildCustomCard(
+                      "Contacts",
+                      Assets.images.iconMoreSettingsBg.image(),
+                      Assets.images.iconMoreSettings
+                          .image(height: 72.w, width: 72.w),
+                      onTap: () {
+                        Get.toNamed(RoutePath.contact);
+                      },
+                    ),
+                    SizedBox(height: 25.h),
                   ],
                 ),
               ),

+ 5 - 0
lib/module/setting/setting_controller.dart

@@ -2,4 +2,9 @@ import 'package:clean/base/base_controller.dart';
 
 class SettingController extends BaseController {
 
+  @override
+  void onInit() {
+    // TODO: implement onInit
+    super.onInit();
+  }
 }

+ 10 - 0
lib/router/app_pages.dart

@@ -1,5 +1,9 @@
 import 'package:clean/module/analysis/analysis_controller.dart';
 import 'package:clean/module/analysis/analysis_view.dart';
+import 'package:clean/module/contact/all/all_controller.dart';
+import 'package:clean/module/contact/all/all_view.dart';
+import 'package:clean/module/contact/contact_controller.dart';
+import 'package:clean/module/contact/contact_view.dart';
 import 'package:clean/module/calendar/calendar_controller.dart';
 import 'package:clean/module/calendar/calendar_month_controller.dart';
 import 'package:clean/module/home/home_controller.dart';
@@ -67,6 +71,8 @@ abstract class RoutePath {
   static const splash = '/splash';
   static const wallpaper = '/wallpaper';
   static const intro = '/intro';
+  static const contact = '/contact';
+  static const contactAll = '/contact/all';
   static const calendarMonth = '/calendarMonth';
 }
 
@@ -93,6 +99,8 @@ class AppBinding extends Bindings {
     lazyPut(() => SplashController());
     lazyPut(() => WallPaperController());
     lazyPut(() => IntroController());
+    lazyPut(() => ContactController());
+    lazyPut(() => AllController());
     lazyPut(() => CalendarController());
     lazyPut(() => CalendarMonthController());
   }
@@ -125,5 +133,7 @@ final generalPages = [
   GetPage(name: RoutePath.splash, page: () => SplashPage()),
   GetPage(name: RoutePath.wallpaper, page: () => WallPaperPage()),
   GetPage(name: RoutePath.intro, page: () => IntroPage()),
+  GetPage(name: RoutePath.contact, page: () => ContactPage()),
+  GetPage(name: RoutePath.contactAll, page: () => AllPage()),
   GetPage(name: RoutePath.calendarMonth, page: () => CalendarMonthPage()),
 ];

+ 16 - 0
pubspec.lock

@@ -441,6 +441,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "7.0.2"
+  flutter_contacts:
+    dependency: "direct main"
+    description:
+      name: flutter_contacts
+      sha256: "388d32cd33f16640ee169570128c933b45f3259bddbfae7a100bb49e5ffea9ae"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.1.9+2"
   flutter_gen_core:
     dependency: transitive
     description:
@@ -1080,6 +1088,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "0.28.0"
+  scrollable_positioned_list:
+    dependency: "direct main"
+    description:
+      name: scrollable_positioned_list
+      sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.3.8"
   sensors_plus:
     dependency: transitive
     description:

+ 4 - 0
pubspec.yaml

@@ -114,6 +114,10 @@ dependencies:
   #网页跳转
   webview_flutter: ^4.10.0
 
+  flutter_contacts: ^1.1.9
+
+  scrollable_positioned_list: ^0.3.8
+
   # 照片
   classify_photo:
     path: plugins/classify_photo