diff --git a/QuickLocation.xcodeproj/project.pbxproj b/QuickLocation.xcodeproj/project.pbxproj index a43daad5..3b1f0ae4 100644 --- a/QuickLocation.xcodeproj/project.pbxproj +++ b/QuickLocation.xcodeproj/project.pbxproj @@ -2972,7 +2972,6 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "AdHoc $(inherited)"; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/QuickLocation/API/GroupAPI.swift b/QuickLocation/API/GroupAPI.swift index aea25a87..c0ffb8d3 100644 --- a/QuickLocation/API/GroupAPI.swift +++ b/QuickLocation/API/GroupAPI.swift @@ -22,6 +22,8 @@ enum GroupAPI { /// - opType: create、dismiss、setdefault /// - requestData:[String: Any] 根据操作传入对应的值 case operate(opType: String, requestData: [String: Any]) + /// 提醒对方开启权限 + case privilegeRemind(title: String, content: String, groupKey: String, userId: String) } extension GroupAPI: MultiTargetProtocol { @@ -36,6 +38,8 @@ extension GroupAPI: MultiTargetProtocol { return "mapi/group/users" case .operate: return "mapi/group/operate" + case .privilegeRemind: + return "mapi/group/privilege/remind" } } @@ -64,6 +68,14 @@ extension GroupAPI: MultiTargetProtocol { params["op_type"] = opType params.merge(requestData) { _, new in new } return .requestParameters(parameters: params, encoding: JSONEncoding()) + + case let .privilegeRemind(title, content, groupKey, userId): + var params = Parameters() + params["title"] = title + params["content"] = content + params["group_key"] = groupKey + params["user_id"] = userId + return .requestParameters(parameters: params, encoding: JSONEncoding()) } } } diff --git a/QuickLocation/API/UserAPI.swift b/QuickLocation/API/UserAPI.swift index 468a957b..75322a0b 100644 --- a/QuickLocation/API/UserAPI.swift +++ b/QuickLocation/API/UserAPI.swift @@ -228,7 +228,7 @@ extension UserAPI: MultiTargetProtocol { switch self { case .userInfo, .userStatus, .phoneUsageToday, .phoneUsagePeriod, .phoneUsageReport, .phoneUsageTrips, .queryPhoneUsageIcon, .phoneLockApps, .phoneLocked, .phoneUnlockRequests, .phoneUnlockAllow, .signInInfo, .notice, .followList, .relations: return .get - case .changePhone, .setGender, .changeAvater: + case .changePhone, .changeAvater: return .put case .deleteAccount: return .delete diff --git a/QuickLocation/Assets.xcassets/Common/image_broken.imageset/Contents.json b/QuickLocation/Assets.xcassets/Common/image_broken.imageset/Contents.json new file mode 100644 index 00000000..a18c0099 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Common/image_broken.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "image_broken@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "image_broken@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/Common/image_broken.imageset/image_broken@2x.png b/QuickLocation/Assets.xcassets/Common/image_broken.imageset/image_broken@2x.png new file mode 100644 index 00000000..d9d38de9 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Common/image_broken.imageset/image_broken@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Common/image_broken.imageset/image_broken@3x.png b/QuickLocation/Assets.xcassets/Common/image_broken.imageset/image_broken@3x.png new file mode 100644 index 00000000..ea39d89d Binary files /dev/null and b/QuickLocation/Assets.xcassets/Common/image_broken.imageset/image_broken@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Common/navBar_bg_1.imageset/navBar_bg_1@2x.png b/QuickLocation/Assets.xcassets/Common/navBar_bg_1.imageset/navBar_bg_1@2x.png index b4f3febe..5fec80b9 100644 Binary files a/QuickLocation/Assets.xcassets/Common/navBar_bg_1.imageset/navBar_bg_1@2x.png and b/QuickLocation/Assets.xcassets/Common/navBar_bg_1.imageset/navBar_bg_1@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Common/navBar_bg_1.imageset/navBar_bg_1@3x.png b/QuickLocation/Assets.xcassets/Common/navBar_bg_1.imageset/navBar_bg_1@3x.png index 2c37e666..27d2883b 100644 Binary files a/QuickLocation/Assets.xcassets/Common/navBar_bg_1.imageset/navBar_bg_1@3x.png and b/QuickLocation/Assets.xcassets/Common/navBar_bg_1.imageset/navBar_bg_1@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Group/invite_card_bg.imageset/组 47999@2x.png b/QuickLocation/Assets.xcassets/Group/invite_card_bg.imageset/组 47999@2x.png index e1651913..2f35a60a 100644 Binary files a/QuickLocation/Assets.xcassets/Group/invite_card_bg.imageset/组 47999@2x.png and b/QuickLocation/Assets.xcassets/Group/invite_card_bg.imageset/组 47999@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Group/invite_card_bg.imageset/组 47999@3x.png b/QuickLocation/Assets.xcassets/Group/invite_card_bg.imageset/组 47999@3x.png index bf7d30fb..da023970 100644 Binary files a/QuickLocation/Assets.xcassets/Group/invite_card_bg.imageset/组 47999@3x.png and b/QuickLocation/Assets.xcassets/Group/invite_card_bg.imageset/组 47999@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_map.imageset/组 48007@2x.png b/QuickLocation/Assets.xcassets/Home/member_map.imageset/组 48007@2x.png index ea03309d..56b93a46 100644 Binary files a/QuickLocation/Assets.xcassets/Home/member_map.imageset/组 48007@2x.png and b/QuickLocation/Assets.xcassets/Home/member_map.imageset/组 48007@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_map.imageset/组 48007@3x.png b/QuickLocation/Assets.xcassets/Home/member_map.imageset/组 48007@3x.png index 81d489c7..d449abad 100644 Binary files a/QuickLocation/Assets.xcassets/Home/member_map.imageset/组 48007@3x.png and b/QuickLocation/Assets.xcassets/Home/member_map.imageset/组 48007@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/report_screen_tip_arrow.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/report_screen_tip_arrow.imageset/Contents.json new file mode 100644 index 00000000..8b36ff41 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/report_screen_tip_arrow.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "report_screen_tip_arrow@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "report_screen_tip_arrow@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/Home/report_screen_tip_arrow.imageset/report_screen_tip_arrow@2x.png b/QuickLocation/Assets.xcassets/Home/report_screen_tip_arrow.imageset/report_screen_tip_arrow@2x.png new file mode 100644 index 00000000..33b0d6d6 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/report_screen_tip_arrow.imageset/report_screen_tip_arrow@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/report_screen_tip_arrow.imageset/report_screen_tip_arrow@3x.png b/QuickLocation/Assets.xcassets/Home/report_screen_tip_arrow.imageset/report_screen_tip_arrow@3x.png new file mode 100644 index 00000000..e0ea8abe Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/report_screen_tip_arrow.imageset/report_screen_tip_arrow@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/report_usage_tip_arrow.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/report_usage_tip_arrow.imageset/Contents.json new file mode 100644 index 00000000..a2428dbc --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/report_usage_tip_arrow.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "report_usage_tip_arrow@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "report_usage_tip_arrow@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/Home/report_usage_tip_arrow.imageset/report_usage_tip_arrow@2x.png b/QuickLocation/Assets.xcassets/Home/report_usage_tip_arrow.imageset/report_usage_tip_arrow@2x.png new file mode 100644 index 00000000..fe0e573f Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/report_usage_tip_arrow.imageset/report_usage_tip_arrow@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/report_usage_tip_arrow.imageset/report_usage_tip_arrow@3x.png b/QuickLocation/Assets.xcassets/Home/report_usage_tip_arrow.imageset/report_usage_tip_arrow@3x.png new file mode 100644 index 00000000..6173fd47 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/report_usage_tip_arrow.imageset/report_usage_tip_arrow@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/warning.imageset/Group_2220@2x.png b/QuickLocation/Assets.xcassets/Home/warning.imageset/Group_2220@2x.png index 496aa63d..c497a2f0 100644 Binary files a/QuickLocation/Assets.xcassets/Home/warning.imageset/Group_2220@2x.png and b/QuickLocation/Assets.xcassets/Home/warning.imageset/Group_2220@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/warning.imageset/Group_2220@3x.png b/QuickLocation/Assets.xcassets/Home/warning.imageset/Group_2220@3x.png index 80d8bff2..a6484ae8 100644 Binary files a/QuickLocation/Assets.xcassets/Home/warning.imageset/Group_2220@3x.png and b/QuickLocation/Assets.xcassets/Home/warning.imageset/Group_2220@3x.png differ diff --git a/QuickLocation/Assets.xcassets/IM/danger.imageset/Contents.json b/QuickLocation/Assets.xcassets/IM/danger.imageset/Contents.json new file mode 100644 index 00000000..39a0631a --- /dev/null +++ b/QuickLocation/Assets.xcassets/IM/danger.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "danger@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "danger@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/IM/danger.imageset/danger@2x.png b/QuickLocation/Assets.xcassets/IM/danger.imageset/danger@2x.png new file mode 100644 index 00000000..9d8c368d Binary files /dev/null and b/QuickLocation/Assets.xcassets/IM/danger.imageset/danger@2x.png differ diff --git a/QuickLocation/Assets.xcassets/IM/danger.imageset/danger@3x.png b/QuickLocation/Assets.xcassets/IM/danger.imageset/danger@3x.png new file mode 100644 index 00000000..2a8879c1 Binary files /dev/null and b/QuickLocation/Assets.xcassets/IM/danger.imageset/danger@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/Contents.json b/QuickLocation/Assets.xcassets/PrivilegeDenied/Contents.json new file mode 100644 index 00000000..6e965652 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PrivilegeDenied/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/app_usage.imageset/Contents.json b/QuickLocation/Assets.xcassets/PrivilegeDenied/app_usage.imageset/Contents.json new file mode 100644 index 00000000..9c45da6a --- /dev/null +++ b/QuickLocation/Assets.xcassets/PrivilegeDenied/app_usage.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "app_usage@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "app_usage@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/app_usage.imageset/app_usage@2x.png b/QuickLocation/Assets.xcassets/PrivilegeDenied/app_usage.imageset/app_usage@2x.png new file mode 100644 index 00000000..0906f71a Binary files /dev/null and b/QuickLocation/Assets.xcassets/PrivilegeDenied/app_usage.imageset/app_usage@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/app_usage.imageset/app_usage@3x.png b/QuickLocation/Assets.xcassets/PrivilegeDenied/app_usage.imageset/app_usage@3x.png new file mode 100644 index 00000000..4097ff35 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PrivilegeDenied/app_usage.imageset/app_usage@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/lock_apps.imageset/Contents.json b/QuickLocation/Assets.xcassets/PrivilegeDenied/lock_apps.imageset/Contents.json new file mode 100644 index 00000000..e93a8384 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PrivilegeDenied/lock_apps.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "lock_apps@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "lock_apps@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/lock_apps.imageset/lock_apps@2x.png b/QuickLocation/Assets.xcassets/PrivilegeDenied/lock_apps.imageset/lock_apps@2x.png new file mode 100644 index 00000000..b3fab9db Binary files /dev/null and b/QuickLocation/Assets.xcassets/PrivilegeDenied/lock_apps.imageset/lock_apps@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/lock_apps.imageset/lock_apps@3x.png b/QuickLocation/Assets.xcassets/PrivilegeDenied/lock_apps.imageset/lock_apps@3x.png new file mode 100644 index 00000000..ea67b306 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PrivilegeDenied/lock_apps.imageset/lock_apps@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/lock_style.imageset/Contents.json b/QuickLocation/Assets.xcassets/PrivilegeDenied/lock_style.imageset/Contents.json new file mode 100644 index 00000000..299e7c12 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PrivilegeDenied/lock_style.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "lock_style@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "lock_style@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/lock_style.imageset/lock_style@2x.png b/QuickLocation/Assets.xcassets/PrivilegeDenied/lock_style.imageset/lock_style@2x.png new file mode 100644 index 00000000..2f8690b1 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PrivilegeDenied/lock_style.imageset/lock_style@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/lock_style.imageset/lock_style@3x.png b/QuickLocation/Assets.xcassets/PrivilegeDenied/lock_style.imageset/lock_style@3x.png new file mode 100644 index 00000000..8fa43613 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PrivilegeDenied/lock_style.imageset/lock_style@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/phone_report.imageset/Contents.json b/QuickLocation/Assets.xcassets/PrivilegeDenied/phone_report.imageset/Contents.json new file mode 100644 index 00000000..74f87202 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PrivilegeDenied/phone_report.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "phone_report@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "phone_report@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/phone_report.imageset/phone_report@2x.png b/QuickLocation/Assets.xcassets/PrivilegeDenied/phone_report.imageset/phone_report@2x.png new file mode 100644 index 00000000..0b36d865 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PrivilegeDenied/phone_report.imageset/phone_report@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/phone_report.imageset/phone_report@3x.png b/QuickLocation/Assets.xcassets/PrivilegeDenied/phone_report.imageset/phone_report@3x.png new file mode 100644 index 00000000..9902c836 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PrivilegeDenied/phone_report.imageset/phone_report@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/report_screen.imageset/Contents.json b/QuickLocation/Assets.xcassets/PrivilegeDenied/report_screen.imageset/Contents.json new file mode 100644 index 00000000..d96722fb --- /dev/null +++ b/QuickLocation/Assets.xcassets/PrivilegeDenied/report_screen.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "report_screen@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "report_screen@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/report_screen.imageset/report_screen@2x.png b/QuickLocation/Assets.xcassets/PrivilegeDenied/report_screen.imageset/report_screen@2x.png new file mode 100644 index 00000000..56debee7 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PrivilegeDenied/report_screen.imageset/report_screen@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/report_screen.imageset/report_screen@3x.png b/QuickLocation/Assets.xcassets/PrivilegeDenied/report_screen.imageset/report_screen@3x.png new file mode 100644 index 00000000..f9476181 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PrivilegeDenied/report_screen.imageset/report_screen@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/report_status.imageset/Contents.json b/QuickLocation/Assets.xcassets/PrivilegeDenied/report_status.imageset/Contents.json new file mode 100644 index 00000000..28c029b2 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PrivilegeDenied/report_status.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "report_status@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "report_status@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/report_status.imageset/report_status@2x.png b/QuickLocation/Assets.xcassets/PrivilegeDenied/report_status.imageset/report_status@2x.png new file mode 100644 index 00000000..94f59e98 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PrivilegeDenied/report_status.imageset/report_status@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/report_status.imageset/report_status@3x.png b/QuickLocation/Assets.xcassets/PrivilegeDenied/report_status.imageset/report_status@3x.png new file mode 100644 index 00000000..e6a9998d Binary files /dev/null and b/QuickLocation/Assets.xcassets/PrivilegeDenied/report_status.imageset/report_status@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/report_usage.imageset/Contents.json b/QuickLocation/Assets.xcassets/PrivilegeDenied/report_usage.imageset/Contents.json new file mode 100644 index 00000000..61feb918 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PrivilegeDenied/report_usage.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "report_usage@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "report_usage@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/report_usage.imageset/report_usage@2x.png b/QuickLocation/Assets.xcassets/PrivilegeDenied/report_usage.imageset/report_usage@2x.png new file mode 100644 index 00000000..f8e713ad Binary files /dev/null and b/QuickLocation/Assets.xcassets/PrivilegeDenied/report_usage.imageset/report_usage@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/report_usage.imageset/report_usage@3x.png b/QuickLocation/Assets.xcassets/PrivilegeDenied/report_usage.imageset/report_usage@3x.png new file mode 100644 index 00000000..0c39767b Binary files /dev/null and b/QuickLocation/Assets.xcassets/PrivilegeDenied/report_usage.imageset/report_usage@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/usage_duration.imageset/Contents.json b/QuickLocation/Assets.xcassets/PrivilegeDenied/usage_duration.imageset/Contents.json new file mode 100644 index 00000000..c7325c02 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PrivilegeDenied/usage_duration.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "usage_duration@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "usage_duration@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/usage_duration.imageset/usage_duration@2x.png b/QuickLocation/Assets.xcassets/PrivilegeDenied/usage_duration.imageset/usage_duration@2x.png new file mode 100644 index 00000000..c9660150 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PrivilegeDenied/usage_duration.imageset/usage_duration@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/usage_duration.imageset/usage_duration@3x.png b/QuickLocation/Assets.xcassets/PrivilegeDenied/usage_duration.imageset/usage_duration@3x.png new file mode 100644 index 00000000..dd70c484 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PrivilegeDenied/usage_duration.imageset/usage_duration@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/usage_frequent.imageset/Contents.json b/QuickLocation/Assets.xcassets/PrivilegeDenied/usage_frequent.imageset/Contents.json new file mode 100644 index 00000000..9c19241c --- /dev/null +++ b/QuickLocation/Assets.xcassets/PrivilegeDenied/usage_frequent.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "usage_frequent@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "usage_frequent@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/usage_frequent.imageset/usage_frequent@2x.png b/QuickLocation/Assets.xcassets/PrivilegeDenied/usage_frequent.imageset/usage_frequent@2x.png new file mode 100644 index 00000000..9f9fbecd Binary files /dev/null and b/QuickLocation/Assets.xcassets/PrivilegeDenied/usage_frequent.imageset/usage_frequent@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PrivilegeDenied/usage_frequent.imageset/usage_frequent@3x.png b/QuickLocation/Assets.xcassets/PrivilegeDenied/usage_frequent.imageset/usage_frequent@3x.png new file mode 100644 index 00000000..4af4dcc3 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PrivilegeDenied/usage_frequent.imageset/usage_frequent@3x.png differ diff --git a/QuickLocation/Component/PrivilegeDeniedOverlay.swift b/QuickLocation/Component/PrivilegeDeniedOverlay.swift index 1dd6c75c..1b4d73da 100644 --- a/QuickLocation/Component/PrivilegeDeniedOverlay.swift +++ b/QuickLocation/Component/PrivilegeDeniedOverlay.swift @@ -9,63 +9,63 @@ final class PrivilegeDeniedOverlay: UIView { var onAction: (() -> Void)? - private let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) - private let dimView = UIView() + private let imageView = UIImageView() private let titleLab = UILabel() private let messageLab = UILabel() private let actionBtn = UIButton(type: .custom) + private let contentStack = UIStackView() override init(frame: CGRect) { super.init(frame: frame) isHidden = true clipsToBounds = true - blurView.isUserInteractionEnabled = false - addSubview(blurView) - blurView.layoutChain.edges() - - dimView.backgroundColor = UIColor.white.withAlphaComponent(0.45) - dimView.isUserInteractionEnabled = false - addSubview(dimView) - dimView.layoutChain.edges() + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + imageView.isUserInteractionEnabled = false + addSubview(imageView) + imageView.layoutChain.edges() titleLab.font = .systemFont(ofSize: 16, weight: .semibold) titleLab.textColor = UIColor(hexStr: "#293445") titleLab.textAlignment = .center - addSubview(titleLab) messageLab.font = .systemFont(ofSize: 16, weight: .semibold) messageLab.textColor = UIColor(hexStr: "#293445") messageLab.textAlignment = .center - addSubview(messageLab) actionBtn.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold) actionBtn.setTitleColor(.white, for: .normal) actionBtn.backgroundColor = UIColor(hexStr: "#16B3FF") actionBtn.cornerRadius = 10 actionBtn.addTarget(self, action: #selector(tapAction), for: .touchUpInside) - addSubview(actionBtn) + actionBtn.layoutChain.width(160).height(44) - titleLab.layoutChain.centerX() - titleLab.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -28).isActive = true - messageLab.layoutChain - .topToBottomOfView(titleLab, offset: 6) - .centerX() - actionBtn.layoutChain - .topToBottomOfView(messageLab, offset: 16) - .centerX() - .width(160) - .height(44) + contentStack.axis = .vertical + contentStack.alignment = .center + contentStack.spacing = 6 + contentStack.addArrangedSubview(titleLab) + contentStack.addArrangedSubview(messageLab) + contentStack.setCustomSpacing(16, after: messageLab) + contentStack.addArrangedSubview(actionBtn) + addSubview(contentStack) + contentStack.layoutChain.centerX().centerY() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - func configure(title: String, message: String, actionTitle: String) { + func configure(title: String, message: String, actionTitle: String, imageName: String) { + imageView.image = UIImage(named: imageName) titleLab.text = title messageLab.text = message actionBtn.setTitle(actionTitle, for: .normal) + let showsContent = !title.isEmpty || !message.isEmpty || !actionTitle.isEmpty + contentStack.isHidden = !showsContent + actionBtn.isHidden = actionTitle.isEmpty + titleLab.isHidden = title.isEmpty + messageLab.isHidden = message.isEmpty } func setVisible(_ visible: Bool) { diff --git a/QuickLocation/Core/Extension/UIDevice+Extension.swift b/QuickLocation/Core/Extension/UIDevice+Extension.swift index 058ff1b7..1dc02391 100644 --- a/QuickLocation/Core/Extension/UIDevice+Extension.swift +++ b/QuickLocation/Core/Extension/UIDevice+Extension.swift @@ -21,7 +21,7 @@ extension UIDevice { deviceModelMap[machineIdentifier] ?? "iPhone" } - /// 与系统状态栏一致的电量百分比(四舍五入);不可用时返回 `nil` + /// 本机电量百分比。公开 API 只有 `UIDevice.batteryLevel`,精度约 5%,无法与状态栏 1% 完全对齐。 static var batteryPercent: Int? { let device = UIDevice.current if !device.isBatteryMonitoringEnabled { @@ -29,7 +29,7 @@ extension UIDevice { } let level = device.batteryLevel guard level >= 0 else { return nil } - return Int((level * 100).rounded()) + return Int((Double(level) * 100).rounded()) } /// 是否正在充电 / 已充满接电 diff --git a/QuickLocation/Manager/Account/UserConfigModel.swift b/QuickLocation/Manager/Account/UserConfigModel.swift index 3c928a6d..e130421e 100644 --- a/QuickLocation/Manager/Account/UserConfigModel.swift +++ b/QuickLocation/Manager/Account/UserConfigModel.swift @@ -54,7 +54,7 @@ struct UserConfigModel: Mappable { temp <- map["temp"] phone <- map["phone"] name <- map["name"] - uid <- map["user_id"] + uid <- (map["user_id"], kFlexString) avater <- map["avater"] sex <- map["sex"] head_pic <- (map["head_pic"], kFlexString) diff --git a/QuickLocation/Manager/MQTT/MQTTService.swift b/QuickLocation/Manager/MQTT/MQTTService.swift index 6a48b295..b47ddcd6 100644 --- a/QuickLocation/Manager/MQTT/MQTTService.swift +++ b/QuickLocation/Manager/MQTT/MQTTService.swift @@ -25,6 +25,8 @@ enum MqttType: String, Codable { case kick = "kick" // 踢人 case signIn = "signIn" // 签到 case sos = "sos" // 求助 + case sosQuit = "sosquit" // 取消求助 + case bubbleQuit = "bubblequit" // 退出气泡 case needTrack = "needtrack" // 追踪上报 case emote = "emote" // 接收表情 case phone = "phone" // 手机信息上报 @@ -1022,9 +1024,7 @@ final class MQTTService: NSObject { func reportLocation(lat: Double, lon: Double, addr: String, speed: CLLocationSpeed, bearing: CLLocationDirection, altitude: CLLocationDistance, accuracy: CLLocationAccuracy) { - let battery = UIDevice.current.batteryLevel > 0 - ? Int(UIDevice.current.batteryLevel * 100) - : 0 + let battery = UIDevice.batteryPercent.map(String.init) ?? "" let point = Points( lat: lat, lon: lon, addr: addr, @@ -1032,7 +1032,7 @@ final class MQTTService: NSObject { speed: speed, bearing: bearing, altitude: altitude, accuracy: accuracy ) - let location = MqttLocation(battery: battery.string, points: [point]) + let location = MqttLocation(battery: battery, points: [point]) guard let jsonData = try? JSONEncoder().encode(location), let dataDict = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] diff --git a/QuickLocation/Manager/MQTT/PhoneStatusCollector.swift b/QuickLocation/Manager/MQTT/PhoneStatusCollector.swift index 21204a87..e4ba1224 100644 --- a/QuickLocation/Manager/MQTT/PhoneStatusCollector.swift +++ b/QuickLocation/Manager/MQTT/PhoneStatusCollector.swift @@ -7,6 +7,7 @@ import AVFoundation import CoreTelephony import Foundation import Network +import NetworkExtension import UIKit struct PhoneStatusSnapshot: Equatable { @@ -67,7 +68,7 @@ final class PhoneStatusCollector: NSObject { ) center.addObserver( self, - selector: #selector(handleChange), + selector: #selector(handleApplicationDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil ) @@ -119,6 +120,11 @@ final class PhoneStatusCollector: NSObject { emitChange() } + @objc private func handleApplicationDidBecomeActive() { + networkMonitor.refreshWiFiName() + emitChange() + } + private func emitChange() { let work = { self.pendingNotify?.cancel() @@ -163,41 +169,68 @@ final class PhoneStatusCollector: NSObject { private final class PhoneNetworkStatusMonitor { - private let monitor = NWPathMonitor() + private let monitor = Network.NWPathMonitor() private let queue = DispatchQueue(label: "com.quicklocation.phone.network") private let lock = NSLock() private let telephonyInfo = CTTelephonyNetworkInfo() - private var path: NWPath? + private var path: Network.NWPath? + private var wifiName: String? private var onChange: (() -> Void)? func start(onChange: @escaping () -> Void) { self.onChange = onChange monitor.pathUpdateHandler = { [weak self] path in - self?.lock.lock() - self?.path = path - self?.lock.unlock() + guard let self else { return } + self.lock.lock() + self.path = path + self.lock.unlock() + if path.status == .satisfied, + path.usesInterfaceType(Network.NWInterface.InterfaceType.wifi) { + self.refreshWiFiName() + } else { + self.updateWiFiName(nil) + } onChange() } monitor.start(queue: queue) } + func refreshWiFiName() { + NEHotspotNetwork.fetchCurrent { [weak self] network in + self?.updateWiFiName(network?.ssid) + } + } + var statusText: String { lock.lock() let currentPath = path + let currentWiFiName = wifiName lock.unlock() guard let currentPath, currentPath.status == .satisfied else { return "无网络" } - if currentPath.usesInterfaceType(.wifi) { - return "Wi-Fi" + if currentPath.usesInterfaceType(Network.NWInterface.InterfaceType.wifi) { + return currentWiFiName ?? "Wi-Fi" } - guard currentPath.usesInterfaceType(.cellular) else { + guard currentPath.usesInterfaceType(Network.NWInterface.InterfaceType.cellular) else { return "无网络" } return cellularGeneration } + private func updateWiFiName(_ name: String?) { + let normalizedName = name?.trimmingCharacters(in: .whitespacesAndNewlines) + let newName = normalizedName?.isEmpty == false ? normalizedName : nil + lock.lock() + let changed = wifiName != newName + wifiName = newName + lock.unlock() + if changed { + onChange?() + } + } + private var cellularGeneration: String { let technologies = telephonyInfo.serviceCurrentRadioAccessTechnology .map { Array($0.values) } ?? [] diff --git a/QuickLocation/Model/GroupModel.swift b/QuickLocation/Model/GroupModel.swift index d1b05cdb..6ff0fbc9 100644 --- a/QuickLocation/Model/GroupModel.swift +++ b/QuickLocation/Model/GroupModel.swift @@ -92,6 +92,7 @@ struct GroupUsersResponse: BaseModelProtocol { var message: String? var model: GroupInfoModel? var list: [GroupMemberModel] = [] + var reviewCount: Int = 0 init?(map: Map) {} @@ -100,6 +101,10 @@ struct GroupUsersResponse: BaseModelProtocol { message <- map["message"] model <- map["data.group"] list <- map["data.mates"] + reviewCount <- (map["data.reviewCount"], kStrTransformInt) + if reviewCount == 0 { + reviewCount <- (map["data.review_count"], kStrTransformInt) + } } func isValid(for groupKey: String) -> Bool { @@ -310,11 +315,11 @@ extension UIImageView { kf.cancelDownloadTask() image = fallback guard let imageURL = URL(string: url), !url.isEmpty else { return } - kf.setImage(with: imageURL, placeholder: fallback) { [weak self] result in - if case .failure = result { - self?.image = fallback - } - } + kf.setImage( + with: imageURL, + placeholder: fallback, + options: [.onFailureImage(kBrokenNetworkImage)] + ) } } @@ -478,7 +483,7 @@ struct GroupMemberModel: Mappable, Equatable { } mutating func mapping(map: Map) { - user_id <- map["user_id"] + user_id <- (map["user_id"], kFlexString) head_pic <- (map["head_pic"], kFlexString) avater <- (map["avater"], kFlexString) if avater.isEmpty { diff --git a/QuickLocation/Plugin/ImagePlugin.swift b/QuickLocation/Plugin/ImagePlugin.swift index a9fc242b..46e19a06 100644 --- a/QuickLocation/Plugin/ImagePlugin.swift +++ b/QuickLocation/Plugin/ImagePlugin.swift @@ -9,6 +9,16 @@ import UIKit import Kingfisher import CoreMedia +nonisolated(unsafe) let kBrokenNetworkImage = UIImage(named: "Common/image_broken") ?? UIImage() + +extension UIImage { + static var brokenNetworkImage: UIImage { kBrokenNetworkImage } +} + +private let kNetworkImageFailureOptions: KingfisherOptionsInfo = [ + .onFailureImage(kBrokenNetworkImage) +] + public protocol ImageSource { /// 图片资源URL var imageURL: URL? { get } @@ -60,15 +70,18 @@ extension DLWrapper where Base: UIImageView { base.image = image return } - // 都不存在,则置为nil - base.image = placeholder + base.image = placeholder ?? .brokenNetworkImage return } // 如果是视频, 获取视频第一帧 if imageURL.imageStr?.contains(".mp4") == true { let source = AVAssetImageDataProvider(assetURL: imageURL, time: CMTimeMakeWithSeconds(0.0, preferredTimescale: 1)) - base.kf.setImage(with: source.convertToSource(), placeholder: placeholder) { result in + base.kf.setImage( + with: source.convertToSource(), + placeholder: placeholder, + options: kNetworkImageFailureOptions + ) { result in switch result { case let .success(imageinfo): completionHandler?(imageinfo.image, nil) @@ -77,8 +90,11 @@ extension DLWrapper where Base: UIImageView { } } } else { - // 使用kf加载图片 - base.kf.setImage(with: imageURL, placeholder: placeholder) { result in + base.kf.setImage( + with: imageURL, + placeholder: placeholder, + options: kNetworkImageFailureOptions + ) { result in switch result { case let .success(imageinfo): completionHandler?(imageinfo.image, nil) @@ -116,18 +132,23 @@ extension DLWrapper where Base: UIButton { return } // 都不存在,则置为nil - base.setImage(placeholder, for: state) + base.setImage(placeholder ?? .brokenNetworkImage, for: state) return } - // 使用kf加载图片 - base.kf.setImage(with: imageURL, for: state, placeholder: placeholder, completionHandler: { result in - switch result { - case let .success(imageinfo): - completionHandler?(imageinfo.image, nil) - case let .failure(error): - completionHandler?(nil, error) + base.kf.setImage( + with: imageURL, + for: state, + placeholder: placeholder, + options: kNetworkImageFailureOptions, + completionHandler: { result in + switch result { + case let .success(imageinfo): + completionHandler?(imageinfo.image, nil) + case let .failure(error): + completionHandler?(nil, error) + } } - }) + ) } /// 设置按钮背景图片 @@ -153,18 +174,23 @@ extension DLWrapper where Base: UIButton { return } // 都不存在,则置为nil - base.setBackgroundImage(placeholder, for: state) + base.setBackgroundImage(placeholder ?? .brokenNetworkImage, for: state) return } - // 使用kf加载图片 - base.kf.setBackgroundImage(with: imageURL, for: state, placeholder: placeholder, completionHandler: { result in - switch result { - case let .success(imageinfo): - completionHandler?(imageinfo.image, nil) - case let .failure(error): - completionHandler?(nil, error) + base.kf.setBackgroundImage( + with: imageURL, + for: state, + placeholder: placeholder, + options: kNetworkImageFailureOptions, + completionHandler: { result in + switch result { + case let .success(imageinfo): + completionHandler?(imageinfo.image, nil) + case let .failure(error): + completionHandler?(nil, error) + } } - }) + ) } } @@ -220,7 +246,7 @@ enum HeadPic { case let .success(value): image = value.image case .failure: - image = UIImage() + image = .brokenNetworkImage } DispatchQueue.main.async { completion(image) @@ -239,8 +265,7 @@ extension UIImageView { } if let url = HeadPic.remoteURL(for: trimmed) { kf.cancelDownloadTask() - image = nil - kf.setImage(with: url, placeholder: nil) + kf.setImage(with: url, placeholder: nil, options: kNetworkImageFailureOptions) } else { kf.cancelDownloadTask() image = HeadPic.localImage(trimmed) diff --git a/QuickLocation/Section/Explore/FeatureIntroVC.swift b/QuickLocation/Section/Explore/FeatureIntroVC.swift index e70f6646..165e3f6b 100644 --- a/QuickLocation/Section/Explore/FeatureIntroVC.swift +++ b/QuickLocation/Section/Explore/FeatureIntroVC.swift @@ -34,6 +34,7 @@ final class FeatureIntroVC: BaseViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + rootView.reloadFeatureItems() guard let config = AppContextManager.shared.systemConfig else { return } bannerList = config.bannerList rootView.cycleScrollView.isHidden = config.bannerList.filter { $0.enable == true }.count == 0 @@ -56,6 +57,7 @@ final class FeatureIntroVC: BaseViewController { case .pigeonMessage: AppRouter.push(Route.pigeonMessage) case .searchLocation: + guard AppContextManager.shared.systemConfig?.isIntercept != true else { return } AppRouter.push(Route.searchLocation) case .sos: AppRouter.push(Route.sos) diff --git a/QuickLocation/Section/Explore/FeatureIntroView.swift b/QuickLocation/Section/Explore/FeatureIntroView.swift index 7b2d848a..33794c66 100644 --- a/QuickLocation/Section/Explore/FeatureIntroView.swift +++ b/QuickLocation/Section/Explore/FeatureIntroView.swift @@ -37,7 +37,7 @@ final class FeatureIntroView: UIView { private let contentView = UIView() private let gridStack = UIStackView() - private let items: [FeatureIntroItem] = [ + private static let allItems: [FeatureIntroItem] = [ FeatureIntroItem( imageName: "Explore/sign_in_card", accessibilityTitle: "还在吗", @@ -76,6 +76,8 @@ final class FeatureIntroView: UIView { ) ] + private var displayedItems: [FeatureIntroItem] = [] + override init(frame: CGRect) { super.init(frame: frame) setupUI() @@ -118,25 +120,7 @@ final class FeatureIntroView: UIView { gridStack.spacing = 13 contentView.addSubview(gridStack) gridStack.translatesAutoresizingMaskIntoConstraints = false - - for rowIndex in 0..<3 { - let rowStack = UIStackView() - rowStack.axis = .horizontal - rowStack.alignment = .fill - rowStack.distribution = .fillEqually - rowStack.spacing = 13 - - for columnIndex in 0..<2 { - let itemIndex = rowIndex * 2 + columnIndex - let item = items[itemIndex] - let card = FeatureIntroCardView(item: item) - card.tag = itemIndex - card.addTarget(self, action: #selector(cardTapped(_:)), for: .touchUpInside) - rowStack.addArrangedSubview(card) - card.heightAnchor.constraint(equalTo: card.widthAnchor, multiplier: item.aspectRatio).isActive = true - } - gridStack.addArrangedSubview(rowStack) - } + reloadFeatureItems() headerBackgroundView.layoutChain .edges(excludingEdge: .bottom) @@ -169,9 +153,47 @@ final class FeatureIntroView: UIView { ]) } + func reloadFeatureItems() { + let hideSearch = AppContextManager.shared.systemConfig?.isIntercept == true + displayedItems = Self.allItems.filter { !hideSearch || $0.action != .searchLocation } + rebuildGrid() + } + + private func rebuildGrid() { + gridStack.arrangedSubviews.forEach { + gridStack.removeArrangedSubview($0) + $0.removeFromSuperview() + } + + let columns = 2 + var index = 0 + while index < displayedItems.count { + let rowStack = UIStackView() + rowStack.axis = .horizontal + rowStack.alignment = .fill + rowStack.distribution = .fillEqually + rowStack.spacing = 13 + + for _ in 0.. Void)? var onReply: (() -> Void)? + var onReport: (() -> Void)? var onDismiss: (() -> Void)? private let panel = UIView() @@ -1025,7 +1026,7 @@ final class ChatBubbleActionMenu: UIView { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - func show(in host: UIView, anchor: UIView, canMention: Bool) { + func show(in host: UIView, anchor: UIView, canMention: Bool, canReport: Bool) { removeFromSuperview() host.addSubview(self) frame = host.bounds @@ -1039,6 +1040,9 @@ final class ChatBubbleActionMenu: UIView { stack.addArrangedSubview(makeButton(title: "TA", imageName: "IM/at", action: #selector(handleMention))) } stack.addArrangedSubview(makeButton(title: "回复", imageName: "IM/reply", action: #selector(handleReply))) + if canReport { + stack.addArrangedSubview(makeButton(title: "举报", imageName: "IM/danger", action: #selector(handleReport))) + } let panelW = itemWidth * CGFloat(max(stack.arrangedSubviews.count, 1)) let panelH = panelHeight @@ -1111,6 +1115,11 @@ final class ChatBubbleActionMenu: UIView { dismiss() } + @objc private func handleReport() { + onReport?() + dismiss() + } + @objc private func handleDismiss() { dismiss() } func dismiss() { @@ -2086,14 +2095,25 @@ class ChatImageMsgCell: UITableViewCell { private func loadPhoto(url: String, messageId: String) { photoView.kf.cancelDownloadTask() - guard !url.isEmpty else { photoView.image = nil; return } - if let localImage = UIImage(contentsOfFile: url) { - photoView.image = localImage + func applyPhoto(_ image: UIImage?, broken: Bool) { + photoView.contentMode = broken ? .scaleAspectFit : .scaleAspectFill + photoView.image = broken ? .brokenNetworkImage : image + } + guard !url.isEmpty else { + applyPhoto(nil, broken: true) + return + } + if !url.hasPrefix("http"), !url.hasPrefix("https") { + if let localImage = UIImage(contentsOfFile: url) { + applyPhoto(localImage, broken: false) + } else { + applyPhoto(nil, broken: true) + } return } photoView.dl.setImage(with: url) { [weak self] image, _ in - guard let self, self.configuredId == messageId, let image else { return } - self.photoView.image = image + guard let self, self.configuredId == messageId else { return } + applyPhoto(image, broken: image == nil) } } diff --git a/QuickLocation/Section/Group/GroupItineraryView.swift b/QuickLocation/Section/Group/GroupItineraryView.swift index 030f5b50..d46a03b7 100644 --- a/QuickLocation/Section/Group/GroupItineraryView.swift +++ b/QuickLocation/Section/Group/GroupItineraryView.swift @@ -47,7 +47,7 @@ final class GroupItineraryView: UIView { addSubview(createBtn) headerRow.layoutChain - .top(8) + .top(20) .edgesHorzontal(16) .height(28) @@ -223,7 +223,7 @@ extension GroupItineraryView: UITableViewDataSource, UITableViewDelegate { )) if !sectionData.dateText.isEmpty { attr.append(NSAttributedString( - string: " \(sectionData.dateText)", + string: " \(sectionData.dateText)", attributes: [ .font: UIFont.systemFont(ofSize: 13, weight: .medium), .foregroundColor: color @@ -322,13 +322,14 @@ final class GroupItineraryCell: UITableViewCell { endLocationLab.translatesAutoresizingMaskIntoConstraints = false tagScrollView.translatesAutoresizingMaskIntoConstraints = false tagStackView.translatesAutoresizingMaskIntoConstraints = false - + + startMarkerLab.layoutChain + .topToBottomOfView(avatarImg, offset: 10) + .leftToView(nameLab) + .width(20) + .heightToWidth(1) + NSLayoutConstraint.activate([ - startMarkerLab.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 14), - startMarkerLab.topAnchor.constraint(equalTo: avatarImg.bottomAnchor, constant: 10), - startMarkerLab.widthAnchor.constraint(equalToConstant: 20), - startMarkerLab.heightAnchor.constraint(equalToConstant: 20), - startLocationLab.leadingAnchor.constraint(equalTo: startMarkerLab.trailingAnchor, constant: 7), startLocationLab.centerYAnchor.constraint(equalTo: startMarkerLab.centerYAnchor), startLocationLab.widthAnchor.constraint(lessThanOrEqualToConstant: 82), diff --git a/QuickLocation/Section/Group/GroupMemberList/GroupMemberListView.swift b/QuickLocation/Section/Group/GroupMemberList/GroupMemberListView.swift index ff253a36..f9ac040f 100644 --- a/QuickLocation/Section/Group/GroupMemberList/GroupMemberListView.swift +++ b/QuickLocation/Section/Group/GroupMemberList/GroupMemberListView.swift @@ -218,6 +218,8 @@ final class GroupMemberDirectoryCell: UITableViewCell { view.backgroundColor = UIColor(hexStr: "#F1F2F4") view.contentMode = .scaleAspectFill view.layer.cornerRadius = 10 + view.layer.borderWidth = 2 + view.layer.borderColor = UIColor.white.cgColor view.clipsToBounds = true return view }() @@ -338,6 +340,8 @@ final class GroupMemberListCell: UICollectionViewCell { view.backgroundColor = UIColor(hexStr: "#F1F2F4") view.contentMode = .scaleAspectFill view.layer.cornerRadius = 25 + view.layer.borderWidth = 2 + view.layer.borderColor = UIColor.white.cgColor view.clipsToBounds = true return view }() diff --git a/QuickLocation/Section/Group/GroupSetting/GroupSettingVC.swift b/QuickLocation/Section/Group/GroupSetting/GroupSettingVC.swift index 99ee3ccb..546ab8c8 100644 --- a/QuickLocation/Section/Group/GroupSetting/GroupSettingVC.swift +++ b/QuickLocation/Section/Group/GroupSetting/GroupSettingVC.swift @@ -23,6 +23,8 @@ class GroupSettingVC: BaseViewController { private var viewModel: GroupSettingViewModel + private var hasAppeared = false + override func viewDidLoad() { super.viewDidLoad() @@ -30,6 +32,14 @@ class GroupSettingVC: BaseViewController { reactiveAction() requestGroupInfo() } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if hasAppeared { + requestReviewBadge() + } + hasAppeared = true + } // MARK: - Actions private func reactiveAction() { @@ -107,6 +117,11 @@ class GroupSettingVC: BaseViewController { rootView.clearHistoryView.rx.tapGesture.subscribe(onNext: { [weak self] _ in self?.confirmClearHistory() }).disposed(by: disposeBag) + + rootView.reportGroupView.rx.tapGesture.subscribe(onNext: { [weak self] _ in + guard self?.viewModel.groupModel?.is_owner == false else { return } + ReceiveMessageReportPopView.show() + }).disposed(by: disposeBag) // 解散圈子 rootView.dismissGroupView.rx.tapGesture.subscribe(onNext: { _ in @@ -127,6 +142,18 @@ class GroupSettingVC: BaseViewController { }).disposed(by: disposeBag) } + private func requestReviewBadge() { + guard viewModel.groupModel?.is_owner == true else { + rootView.setAuditBadgeVisible(false) + return + } + GroupService.reviewlist(requestData: ["group_key": viewModel.groupId]) + .subscribe(onNext: { [weak self] response in + self?.rootView.setAuditBadgeVisible(!response.list.isEmpty) + }, onError: { _ in }) + .disposed(by: disposeBag) + } + // MARK: - API private func requestGroupInfo() { DLToast.showLoading() @@ -144,7 +171,9 @@ class GroupSettingVC: BaseViewController { self.rootView.setupData(model, members: response.list, groupNickname: self.viewModel.displayGroupNickname) + self.rootView.setAuditBadgeVisible(model.is_owner && response.reviewCount > 0) self.requestGroupNickname() + self.requestReviewBadge() }, onError: { error in DLToast.dismiss() DLToast.showError(text: error.localizedDescription) diff --git a/QuickLocation/Section/Group/GroupSetting/GroupSettingView.swift b/QuickLocation/Section/Group/GroupSetting/GroupSettingView.swift index 5f6270f1..729374cd 100644 --- a/QuickLocation/Section/Group/GroupSetting/GroupSettingView.swift +++ b/QuickLocation/Section/Group/GroupSetting/GroupSettingView.swift @@ -27,8 +27,12 @@ final class GroupSettingView: UIView { switchBtn.isOn = model.review auditSwitchView.isHidden = !model.is_owner auditMemberView.isHidden = !model.is_owner + if !model.is_owner { + auditMemberView.setShowsBadge(false) + } dismissGroupView.isHidden = !model.is_owner leaveGroupView.isHidden = model.is_owner + reportGroupView.isHidden = model.is_owner groupNameView.setShowsArrow(model.is_owner) groupTypeView.setShowsArrow(false) @@ -47,6 +51,10 @@ final class GroupSettingView: UIView { groupNicknameView.setValue(nickname) } + func setAuditBadgeVisible(_ visible: Bool) { + auditMemberView.setShowsBadge(visible) + } + override init(frame: CGRect) { super.init(frame: frame) backgroundColor = UIColor(hexStr: "#F7F8FA") @@ -212,6 +220,7 @@ final class GroupSettingView: UIView { dangerStackView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate(dangerStackView.edgesToSuperview()) dangerStackView.addArrangedSubview(dismissGroupView) + dangerStackView.addArrangedSubview(reportGroupView) dangerStackView.addArrangedSubview(leaveGroupView) } @@ -296,7 +305,7 @@ final class GroupSettingView: UIView { private let memberSeparatorView = HorizontalDashLineView() private let memberActionsView = UIView() - lazy var inviteCodeButton: UIButton = makeMemberActionButton(title: "邀请码", imageName: "GroupSetting/invite_code") + lazy var inviteCodeButton: UIButton = makeMemberActionButton(title: "邀请成员", imageName: "GroupSetting/invite_code") lazy var shareButton: UIButton = makeMemberActionButton(title: "分享", imageName: "GroupSetting/share") private lazy var actionDividerView: UIView = { @@ -343,6 +352,7 @@ final class GroupSettingView: UIView { }() let dismissGroupView = GroupSettingActionRow(title: "解散圈子", titleColor: UIColor(hexStr: "#FF4D53"), hidesDivider: true) + let reportGroupView = GroupSettingActionRow(title: "举报", titleColor: UIColor(hexStr: "#FF4D53")) let leaveGroupView = GroupSettingActionRow(title: "退出圈子", titleColor: UIColor(hexStr: "#FF4D53"), hidesDivider: true) private func makeCardView() -> UIView { @@ -558,6 +568,7 @@ final class GroupSettingInfoRow: UIView { private let style: Style private let titleLab = UILabel() + private let badgeView = UIView() private let valueLab = UILabel() private let valueImageView = UIImageView() private let arrowImageView = UIImageView(image: UIImage(named: "GroupSetting/arrow_right")) @@ -596,6 +607,10 @@ final class GroupSettingInfoRow: UIView { imageTrailingToRow?.isActive = !showsArrow } + func setShowsBadge(_ showsBadge: Bool) { + badgeView.isHidden = !showsBadge + } + private func setupUI() { backgroundColor = .clear titleLab.textColor = UIColor(hexStr: "#293445") @@ -613,14 +628,21 @@ final class GroupSettingInfoRow: UIView { arrowImageView.contentMode = .scaleAspectFit dividerView.backgroundColor = UIColor(hexStr: "#EDEEF0") switchButton.onTintColor = UIColor(hexStr: "#16B3FF") + badgeView.backgroundColor = UIColor(hexStr: "#FF5260") + badgeView.layer.cornerRadius = 4 + badgeView.isHidden = true - [titleLab, valueLab, valueImageView, arrowImageView, switchButton, dividerView].forEach { + [titleLab, badgeView, valueLab, valueImageView, arrowImageView, switchButton, dividerView].forEach { addSubview($0) $0.translatesAutoresizingMaskIntoConstraints = false } NSLayoutConstraint.activate([ titleLab.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + badgeView.trailingAnchor.constraint(equalTo: arrowImageView.leadingAnchor, constant: -4), + badgeView.centerYAnchor.constraint(equalTo: arrowImageView.centerYAnchor), + badgeView.widthAnchor.constraint(equalToConstant: 8), + badgeView.heightAnchor.constraint(equalToConstant: 8), arrowImageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), arrowImageView.centerYAnchor.constraint(equalTo: centerYAnchor), arrowImageView.widthAnchor.constraint(equalToConstant: 16), @@ -733,8 +755,8 @@ final class GroupSettingTagRow: UIView { $0.removeFromSuperview() } if tags.count > 2 { - tagStack.addArrangedSubview(GroupSettingTagChip(text: "···", compact: true)) tags.suffix(2).forEach { tagStack.addArrangedSubview(GroupSettingTagChip(text: $0, compact: false)) } + tagStack.addArrangedSubview(GroupSettingTagChip(text: "···", compact: true)) } else { tags.forEach { tagStack.addArrangedSubview(GroupSettingTagChip(text: $0, compact: false)) } } diff --git a/QuickLocation/Section/Group/GroupView.swift b/QuickLocation/Section/Group/GroupView.swift index cbdcf69e..eb7a877f 100644 --- a/QuickLocation/Section/Group/GroupView.swift +++ b/QuickLocation/Section/Group/GroupView.swift @@ -24,6 +24,13 @@ class GroupView: UIView { var disposeBag = DisposeBag() private let actionCardSpacing: CGFloat = 13 + private let listRowHeight: CGFloat = 65 + private let listBottomSpacing: CGFloat = 15 + private let tabBarContentHeight: CGFloat = 49 + private var listHeightConstraint: NSLayoutConstraint? + private var joinedItemCount = 0 + private var createdItemCount = 0 + private var lastLayoutSize: CGSize = .zero private var actionCardHeight: CGFloat { let availableWidth = UIScreen.main.bounds.width - 32 - actionCardSpacing @@ -63,7 +70,8 @@ class GroupView: UIView { segmentView.addSubview(joinedTabLabel) segmentView.addSubview(createdTabLabel) segmentView.addSubview(tabIndicator) - listContainer.addSubview(segmentScrollView) + listContainer.addSubview(listCardView) + listCardView.addSubview(segmentScrollView) segmentScrollView.addSubview(segmentContentView) // 全部圈子(joined) | 我的圈子(created) segmentContentView.addSubview(joinedTableView) @@ -90,7 +98,7 @@ class GroupView: UIView { circleTabLabel.layoutChain .left(20) - .bottom(10) + .bottom() .height(28) itineraryTabLabel.layoutChain @@ -160,25 +168,30 @@ class GroupView: UIView { .centerX(joinedTabLabel) .width(19).height(4) - segmentScrollView.layoutChain - .topToBottomOfView(segmentView) - .edgesHorzontal() + listCardView.layoutChain + .topToBottomOfView(segmentView, offset: 6) + .edgesHorzontal(15) .bottom() - .height(kScreenHeight - kNaviHeight - 44 - 12 - actionCardHeight - 16) + listHeightConstraint = listCardView.heightAnchor.constraint(equalToConstant: listRowHeight) + listHeightConstraint?.isActive = true + + segmentScrollView.layoutChain + .edges() segmentContentView.layoutChain .edges() .heightToView(segmentScrollView) + .widthToView(segmentScrollView, multiplier: 2) - let svWidth = UIScreen.main.bounds.width joinedTableView.layoutChain .top().bottom().left() - .width(svWidth) + .widthToView(segmentScrollView) createdTableView.layoutChain .top().bottom() .leftToRightOfView(joinedTableView) - .width(svWidth) + .right() + .widthToView(segmentScrollView) // 离屏占位,避免影响布局 cycleScrollView.layoutChain @@ -192,10 +205,9 @@ class GroupView: UIView { override func layoutSubviews() { super.layoutSubviews() - segmentContentView.layoutChain.width(bounds.width * 2) - listContainer.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - listContainer.layer.cornerRadius = 20 - listContainer.clipsToBounds = true + guard bounds.size != lastLayoutSize else { return } + lastLayoutSize = bounds.size + updateListHeight(animated: false) } // MARK: - Nav @@ -350,15 +362,23 @@ class GroupView: UIView { // MARK: - List container private lazy var listContainer: UIView = { + let v = UIView() + v.backgroundColor = .clear + return v + }() + + private lazy var listCardView: UIView = { let v = UIView() v.backgroundColor = .white + v.layer.cornerRadius = 10 + v.clipsToBounds = true return v }() // MARK: - Segment(0 全部圈子=joined,1 我的圈子=created) private(set) lazy var segmentView: UIView = { let v = UIView() - v.backgroundColor = .white + v.backgroundColor = .clear let line = UIView() line.backgroundColor = UIColor(hexStr: "#B5B5B5").withAlphaComponent(0.12) @@ -375,7 +395,7 @@ class GroupView: UIView { private(set) lazy var joinedTabLabel: UILabel = { let label = UILabel() label.text = "全部圈子" - label.font = .systemFont(ofSize: 16, weight: .heavy) + label.font = .systemFont(ofSize: 16, weight: .bold) label.textColor = UIColor(hexStr: "#2A3648") label.textAlignment = .center label.isUserInteractionEnabled = true @@ -387,7 +407,7 @@ class GroupView: UIView { let label = UILabel() label.text = "我的圈子" label.font = .systemFont(ofSize: 16, weight: .medium) - label.textColor = UIColor(hexStr: "#2A3648") + label.textColor = UIColor(hexStr: "#2A3648", alpha: 0.5) label.textAlignment = .center label.isUserInteractionEnabled = true return label @@ -424,9 +444,9 @@ class GroupView: UIView { tv.showsVerticalScrollIndicator = false tv.register(CircleGroupCell.self) tv.estimatedRowHeight = 70 - tv.rowHeight = 70 - tv.bounces = true - tv.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 97 + kSafeBottomMargin, right: 0) + tv.rowHeight = listRowHeight + tv.bounces = false + tv.isScrollEnabled = false return tv }() @@ -437,9 +457,9 @@ class GroupView: UIView { tv.showsVerticalScrollIndicator = false tv.register(CircleGroupCell.self) tv.estimatedRowHeight = 70 - tv.rowHeight = 70 - tv.bounces = true - tv.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 97 + kSafeBottomMargin, right: 0) + tv.rowHeight = listRowHeight + tv.bounces = false + tv.isScrollEnabled = false return tv }() @@ -450,12 +470,13 @@ class GroupView: UIView { func selectSegment(at index: Int) { currentSegmentIndex = index let isAll = index == 0 + updateListHeight(animated: false) UIView.animate(withDuration: 0.2) { self.joinedTabLabel.textColor = isAll ? UIColor(hexStr: "#2A3648") : UIColor(hexStr: "#2A3648", alpha: 0.5) - self.joinedTabLabel.font = .systemFont(ofSize: 16, weight: isAll ? .heavy : .medium) + self.joinedTabLabel.font = .systemFont(ofSize: 16, weight: isAll ? .bold : .medium) self.createdTabLabel.textColor = isAll ? UIColor(hexStr: "#2A3648", alpha: 0.5) : UIColor(hexStr: "#2A3648") - self.createdTabLabel.font = .systemFont(ofSize: 16, weight: isAll ? .medium : .heavy) + self.createdTabLabel.font = .systemFont(ofSize: 16, weight: isAll ? .medium : .bold) self.tabIndicator.center.x = isAll ? self.joinedTabLabel.center.x @@ -465,6 +486,65 @@ class GroupView: UIView { } } + func updateListCount(_ itemCount: Int, forSegment index: Int) { + if index == 0 { + joinedItemCount = itemCount + } else { + createdItemCount = itemCount + } + guard index == currentSegmentIndex else { + updateTableScrolling() + return + } + updateListHeight(animated: true) + } + + private func updateListHeight(animated: Bool) { + guard bounds.height > 0 else { return } + if animated { + layoutIfNeeded() + } + + let itemCount = currentSegmentIndex == 0 ? joinedItemCount : createdItemCount + let contentHeight = CGFloat(max(itemCount, 1)) * listRowHeight + let safeBottom = max(kSafeBottomMargin, safeAreaInsets.bottom) + let tabBarTop = bounds.height - tabBarContentHeight - safeBottom + let cardTop = listCardView.convert(.zero, to: self).y + let maxHeight = max(0, tabBarTop - listBottomSpacing - cardTop) + let targetHeight = min(contentHeight, maxHeight) + + guard abs((listHeightConstraint?.constant ?? 0) - targetHeight) > 0.5 else { + updateTableScrolling(maxHeight: maxHeight) + return + } + listHeightConstraint?.constant = targetHeight + updateTableScrolling(maxHeight: maxHeight) + + guard animated else { return } + UIView.animate(withDuration: 0.2) { + self.layoutIfNeeded() + } + } + + private func updateTableScrolling(maxHeight: CGFloat? = nil) { + let availableHeight: CGFloat + if let maxHeight { + availableHeight = maxHeight + } else { + let safeBottom = max(kSafeBottomMargin, safeAreaInsets.bottom) + let tabBarTop = bounds.height - tabBarContentHeight - safeBottom + let cardTop = listCardView.convert(.zero, to: self).y + availableHeight = max(0, tabBarTop - listBottomSpacing - cardTop) + } + + let joinedScrollable = CGFloat(joinedItemCount) * listRowHeight > availableHeight + 0.5 + let createdScrollable = CGFloat(createdItemCount) * listRowHeight > availableHeight + 0.5 + joinedTableView.isScrollEnabled = joinedScrollable + joinedTableView.bounces = joinedScrollable + createdTableView.isScrollEnabled = createdScrollable + createdTableView.bounces = createdScrollable + } + func selectTopTab(at index: Int) { guard index == 0 || index == 1 else { return } currentTopTab = index @@ -511,8 +591,16 @@ extension GroupView: UIScrollViewDelegate { } } + /// 外层内容刚好铺满时不能吸顶,内层列表必须自己滚动。 + private var isMainScrollable: Bool { + let inset = mainScrollView.adjustedContentInset + return mainScrollView.contentSize.height + inset.top + inset.bottom + > mainScrollView.bounds.height + 0.5 + } + /// 由 GroupViewController 通过 rx.didScroll 转发调用 func handleTableViewScroll(_ scrollView: UIScrollView) { + guard isMainScrollable else { return } if isSubCanScroll { if scrollView.contentOffset.y <= 0 { isSubCanScroll = false @@ -526,6 +614,7 @@ extension GroupView: UIScrollViewDelegate { // MARK: - 主 scroll 逻辑 private func handleMainScroll(_ scrollView: UIScrollView) { + guard isMainScrollable else { return } if isSubCanScroll { scrollView.contentOffset.y = stickThreshold isSticky = true @@ -673,6 +762,12 @@ final class CircleGroupCell: UITableViewCell { return view }() + private let separatorView: UIView = { + let view = UIView() + view.backgroundColor = UIColor(hexStr: "#EEEEEE") + return view + }() + override init(style: CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none @@ -687,6 +782,7 @@ final class CircleGroupCell: UITableViewCell { contentView.addSubview(timeLabel) contentView.addSubview(previewLabel) contentView.addSubview(tagStack) + contentView.addSubview(separatorView) iconView.layoutChain .left(15) @@ -720,6 +816,14 @@ final class CircleGroupCell: UITableViewCell { .topToBottomOfView(nameLabel, offset: 4) .leftToView(nameLabel) .rightToLeftOfView(tagStack, offset: -8) + + separatorView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + separatorView.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor), + separatorView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + separatorView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + separatorView.heightAnchor.constraint(equalToConstant: 1.0 / UIScreen.main.scale) + ]) } required init?(coder: NSCoder) { @@ -731,8 +835,9 @@ final class CircleGroupCell: UITableViewCell { iconView.setGroupIcon(url: "", fallback: nil) } - func configure(_ model: GroupCellData) { + func configure(_ model: GroupCellData, showsSeparator: Bool) { let g = model.group + separatorView.isHidden = !showsSeparator iconView.setGroupIcon(url: model.iconURL, fallback: model.localIcon) nameLabel.text = "\(g.name) (\(g.people_no))" previewLabel.text = model.lastMsgPreview diff --git a/QuickLocation/Section/Group/GroupViewController.swift b/QuickLocation/Section/Group/GroupViewController.swift index b9ae6a6e..e70aa01b 100644 --- a/QuickLocation/Section/Group/GroupViewController.swift +++ b/QuickLocation/Section/Group/GroupViewController.swift @@ -116,11 +116,19 @@ final class GroupViewController: BaseViewController { viewModel.output.createdSections .observe(on: MainScheduler.asyncInstance) + .do(onNext: { [weak self] sections in + let itemCount = sections.reduce(0) { $0 + $1.items.count } + self?.rootView.updateListCount(itemCount, forSegment: 1) + }) .bind(to: rootView.createdTableView.rx.items(dataSource: createdDataSource)) .disposed(by: disposeBag) viewModel.output.joinedSections .observe(on: MainScheduler.asyncInstance) + .do(onNext: { [weak self] sections in + let itemCount = sections.reduce(0) { $0 + $1.items.count } + self?.rootView.updateListCount(itemCount, forSegment: 0) + }) .bind(to: rootView.joinedTableView.rx.items(dataSource: joinedDataSource)) .disposed(by: disposeBag) @@ -354,17 +362,21 @@ final class GroupViewController: BaseViewController { }() private lazy var createdDataSource: RxTableViewSectionedReloadDataSource = { - RxTableViewSectionedReloadDataSource { _, tableView, indexPath, model in + RxTableViewSectionedReloadDataSource { dataSource, tableView, indexPath, model in let cell: CircleGroupCell = tableView.dequeueReusableCell(for: indexPath) - cell.configure(model) + let isLastRow = indexPath.section == dataSource.sectionModels.count - 1 + && indexPath.row == dataSource.sectionModels[indexPath.section].items.count - 1 + cell.configure(model, showsSeparator: !isLastRow) return cell } }() private lazy var joinedDataSource: RxTableViewSectionedReloadDataSource = { - RxTableViewSectionedReloadDataSource { _, tableView, indexPath, model in + RxTableViewSectionedReloadDataSource { dataSource, tableView, indexPath, model in let cell: CircleGroupCell = tableView.dequeueReusableCell(for: indexPath) - cell.configure(model) + let isLastRow = indexPath.section == dataSource.sectionModels.count - 1 + && indexPath.row == dataSource.sectionModels[indexPath.section].items.count - 1 + cell.configure(model, showsSeparator: !isLastRow) return cell } }() diff --git a/QuickLocation/Section/Group/InviteJoin/InviteJoinView.swift b/QuickLocation/Section/Group/InviteJoin/InviteJoinView.swift index d469a915..62bc225d 100644 --- a/QuickLocation/Section/Group/InviteJoin/InviteJoinView.swift +++ b/QuickLocation/Section/Group/InviteJoin/InviteJoinView.swift @@ -85,7 +85,7 @@ class InviteJoinView: UIView { inviteCardView.layoutChain .topToBottomOfView(navView, offset: 42) .edgesHorzontal(15) - .heightToWidth(481/345) + .heightToWidth(518/345) cardBgImg.layoutChain.edges() @@ -116,10 +116,10 @@ class InviteJoinView: UIView { .centerX() qrCodeView.layoutChain - .bottom(42) + .bottom(38) .centerX() - .width(138) - .height(138) + .width(180) + .height(180) qrCodeImgView.layoutChain .edges(all: 18) diff --git a/QuickLocation/Section/Group/Mailbox/MailboxView.swift b/QuickLocation/Section/Group/Mailbox/MailboxView.swift index 1e9cd060..03aa6f74 100644 --- a/QuickLocation/Section/Group/Mailbox/MailboxView.swift +++ b/QuickLocation/Section/Group/Mailbox/MailboxView.swift @@ -121,8 +121,13 @@ final class MailboxView: UIView { headerGradientLayer.locations = [0, 0.58, 1] headerGradientLayer.startPoint = CGPoint(x: 0, y: 0) headerGradientLayer.endPoint = CGPoint(x: 1, y: 1) - layer.insertSublayer(headerGradientLayer, at: 0) +// layer.insertSublayer(headerGradientLayer, at: 0) + addSubview(navBgView) + navBgView.layoutChain + .edges(excludingEdge: .bottom) + .heightToWidth(160/375) + setupNavigationBar() setupCategories() setupTableView() @@ -252,6 +257,13 @@ final class MailboxView: UIView { loadingIndicator.centerYAnchor.constraint(equalTo: emptyView.centerYAnchor, constant: -40) ]) } + + lazy var navBgView: UIImageView = { + let iv = UIImageView() + iv.image = UIImage(named: "Common/navBar_bg_1") + iv.contentMode = .scaleAspectFill + return iv + }() @objc private func categoryTapped(_ sender: MailboxCategoryView) { onSelectCategory?(sender.category) diff --git a/QuickLocation/Section/Home/AppUsageReport/AppUsageReportVC.swift b/QuickLocation/Section/Home/AppUsageReport/AppUsageReportVC.swift index 6028a317..7191374f 100644 --- a/QuickLocation/Section/Home/AppUsageReport/AppUsageReportVC.swift +++ b/QuickLocation/Section/Home/AppUsageReport/AppUsageReportVC.swift @@ -71,6 +71,31 @@ final class AppUsageReportVC: BaseViewController { self?.rootView.apply(snapshot) }) .disposed(by: disposeBag) + + viewModel.selectedMemberId + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] _ in + self?.updatePrivilegeOverlay() + }) + .disposed(by: disposeBag) + + rootView.onRemind = { [weak self] in + guard let self else { return } + GroupService.sendPrivilegeRemind( + content: "请开启App使用记录隐私设置", + groupKey: self.viewModel.groupKey, + userId: self.viewModel.selectedMemberId.value + ) + } + updatePrivilegeOverlay() + } + + private func updatePrivilegeOverlay() { + let userId = viewModel.selectedMemberId.value + let isSelf = userId == AppContextManager.shared.userId + let member = viewModel.members.first { $0.user_id == userId } + let denied = !isSelf && member?.extra.privilege.app_usage_record == false + rootView.setPrivilegeDenied(denied) } } diff --git a/QuickLocation/Section/Home/AppUsageReport/AppUsageReportView.swift b/QuickLocation/Section/Home/AppUsageReport/AppUsageReportView.swift index 9fae692e..56ae258d 100644 --- a/QuickLocation/Section/Home/AppUsageReport/AppUsageReportView.swift +++ b/QuickLocation/Section/Home/AppUsageReport/AppUsageReportView.swift @@ -17,13 +17,14 @@ final class AppUsageReportView: UIView { let view = UICollectionView(frame: .zero, collectionViewLayout: layout) view.backgroundColor = .white view.showsHorizontalScrollIndicator = false - view.contentInset = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12) + view.contentInset = UIEdgeInsets(top: 3, left: 6, bottom: 0, right: 6) view.register(GroupMemberListCell.self) view.applyAppUsageReportCardStyle(cornerRadius: 20) return view }() var onPeriodChanged: ((AppUsageReportPeriod) -> Void)? + var onRemind: (() -> Void)? private let navBgView = UIImageView(image: UIImage(named: "Common/navBar_bg_2")) private let scrollView = UIScrollView() @@ -52,6 +53,12 @@ final class AppUsageReportView: UIView { ) } + func setPrivilegeDenied(_ denied: Bool) { + let remind: () -> Void = { [weak self] in self?.onRemind?() } + frequentSection.setDenied(denied, imageName: "PrivilegeDenied/usage_frequent", onRemind: remind) + durationSection.setDenied(denied, imageName: "PrivilegeDenied/usage_duration", onRemind: remind) + } + private func bindPeriodControls() { let onChange: (AppUsageReportPeriod) -> Void = { [weak self] period in self?.onPeriodChanged?(period) @@ -142,6 +149,7 @@ private final class AppUsageFrequentSectionView: UIView { private let periodControl = AppUsagePeriodControl() private let rowsStack = UIStackView() private let statusLab = UILabel() + private let deniedOverlay = PrivilegeDeniedOverlay() override init(frame: CGRect) { super.init(frame: frame) @@ -214,6 +222,22 @@ private final class AppUsageFrequentSectionView: UIView { statusLab.numberOfLines = 0 cardView.addSubview(statusLab) statusLab.layoutChain.centerX().centerY(cardView, offset: 16).left(24).right(24) + + cardView.addSubview(deniedOverlay) + deniedOverlay.layoutChain.edges() + deniedOverlay.layer.cornerRadius = 20 + } + + func setDenied(_ denied: Bool, imageName: String, onRemind: (() -> Void)?) { + deniedOverlay.configure( + title: "TA未开启设置", + message: "无法查看手机状态", + actionTitle: "提醒TA", + imageName: imageName + ) + deniedOverlay.onAction = onRemind + deniedOverlay.setVisible(denied) + if denied { cardView.bringSubviewToFront(deniedOverlay) } } } @@ -232,6 +256,7 @@ private final class AppUsageDurationSectionView: UIView { private let totalHintLab = UILabel() private let legendStack = UIStackView() private let statusLab = UILabel() + private let deniedOverlay = PrivilegeDeniedOverlay() override init(frame: CGRect) { super.init(frame: frame) @@ -336,6 +361,22 @@ private final class AppUsageDurationSectionView: UIView { statusLab.numberOfLines = 0 cardView.addSubview(statusLab) statusLab.layoutChain.centerX().centerY(cardView, offset: 18).left(24).right(24) + + cardView.addSubview(deniedOverlay) + deniedOverlay.layoutChain.edges() + deniedOverlay.layer.cornerRadius = 20 + } + + func setDenied(_ denied: Bool, imageName: String, onRemind: (() -> Void)?) { + deniedOverlay.configure( + title: "TA未开启设置", + message: "无法查看手机状态", + actionTitle: "提醒TA", + imageName: imageName + ) + deniedOverlay.onAction = onRemind + deniedOverlay.setVisible(denied) + if denied { cardView.bringSubviewToFront(deniedOverlay) } } } diff --git a/QuickLocation/Section/Home/AppUsageReport/AppUsageReportViewModel.swift b/QuickLocation/Section/Home/AppUsageReport/AppUsageReportViewModel.swift index a19564d3..670a315a 100644 --- a/QuickLocation/Section/Home/AppUsageReport/AppUsageReportViewModel.swift +++ b/QuickLocation/Section/Home/AppUsageReport/AppUsageReportViewModel.swift @@ -61,7 +61,7 @@ final class AppUsageReportViewModel { let selectedPeriod = BehaviorRelay(value: .week) let snapshot = BehaviorRelay(value: .init(state: .loading, period: .week, totalUsageTime: 0, apps: [])) - private let groupKey: String + let groupKey: String private let disposeBag = DisposeBag() private var reportCache: [String: PhoneUsagePeriodModel] = [:] private var requestIDs: [String: UUID] = [:] diff --git a/QuickLocation/Section/Home/GroupListPopView.swift b/QuickLocation/Section/Home/GroupListPopView.swift index d716aaa4..b8f732ad 100644 --- a/QuickLocation/Section/Home/GroupListPopView.swift +++ b/QuickLocation/Section/Home/GroupListPopView.swift @@ -172,7 +172,7 @@ class GroupListPopView: UIView { private func updateTableHeight() { let visible = min(groupList.count, Self.maxVisibleRows) let height = CGFloat(visible) * Self.rowHeight - tableView.layoutChain.height(height) + tableView.layoutChain.height(groupList.count > 3 ? height + 30 : height) tableView.isScrollEnabled = groupList.count > Self.maxVisibleRows tableView.reloadData() } @@ -315,11 +315,8 @@ class GroupListCell: UITableViewCell { cardView.layer.borderColor = GroupListPopView.primaryBlueColor.cgColor checkIcon.image = isSelected - ? UIImage(systemName: "checkmark.circle.fill") - : UIImage(systemName: "circle") - checkIcon.tintColor = isSelected - ? GroupListPopView.primaryBlueColor - : UIColor(hexStr: "#D0D0D0") + ? UIImage(named: "Common/checkbox_on") + : UIImage(named: "Common/checkbox") } private static func makeTagLabel(_ text: String) -> UILabel { @@ -365,8 +362,8 @@ class GroupListCell: UITableViewCell { checkIcon.layoutChain .right(12) .centerY() - .width(22) - .height(22) + .width(24) + .height(24) nameLab.layoutChain .topToView(avaterImgView, offset: 2) @@ -379,7 +376,7 @@ class GroupListCell: UITableViewCell { .rightToLeftOfView(checkIcon, offset: -8, relation: .lessThanOrEqual) countLab.layoutChain - .topToBottomOfView(nameLab, offset: 4) + .bottomToView(avaterImgView) .leftToView(nameLab) .rightToLeftOfView(checkIcon, offset: -8) } @@ -437,7 +434,7 @@ class GroupListCell: UITableViewCell { lazy var checkIcon: UIImageView = { let view = UIImageView() - view.contentMode = .scaleAspectFit + view.contentMode = .scaleAspectFill view.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: 20, weight: .regular) return view }() diff --git a/QuickLocation/Section/Home/GroupMemberView2.swift b/QuickLocation/Section/Home/GroupMemberView2.swift index d88f3074..0e83b608 100644 --- a/QuickLocation/Section/Home/GroupMemberView2.swift +++ b/QuickLocation/Section/Home/GroupMemberView2.swift @@ -22,8 +22,13 @@ class GroupMemberView2: UIView { private var messageEchoByUserId: [String: String] = [:] private var phoneStatusObserver: NSObjectProtocol? private var weatherRequestID = UUID() + private var localWeatherSnapshot: MemberWeatherSnapshot? + private var localWeatherSnapshotKey: String? + private var localWeatherUpdatedAt: Date? + private var pendingWeatherLocationKey: String? var onCoupleMemberTap: ((GroupMemberModel) -> Void)? + private static let weatherRefreshInterval: TimeInterval = 15 * 60 private static let batteryNormalColor = UIColor(hexStr: "#47DD00") private static let batteryLowColor = UIColor(hexStr: "#F85A5D") private static let batteryDefaultTextColor = UIColor(hexStr: "#353B4F") @@ -45,10 +50,11 @@ class GroupMemberView2: UIView { ownView.isHidden = !isOwner relationIconView.configure(relationIdx: model.extra.relation_idx) updateMoodBadge(model.mood) - statusDotView.backgroundColor = UIColor(hexStr: model.is_online ? "#67EA76" : "#D8D8D8") - statusLab.text = model.is_online ? "在线" : "离线" - let isCurrentUser = model.user_id == AppContextManager.shared.userId + let isCurrentUser = Self.isCurrentUser(model.user_id) + let isOnline = isCurrentUser || model.is_online + statusDotView.backgroundColor = UIColor(hexStr: isOnline ? "#67EA76" : "#D8D8D8") + statusLab.text = isOnline ? "在线" : "离线" if !model.is_online, !isCurrentUser { locationLab.text = "**" distanceView.isHidden = true @@ -59,6 +65,8 @@ class GroupMemberView2: UIView { applyPhoneUsage(phoneUsage) if isCurrentUser { + applyLocalDeviceInfo() + startLocalPhoneStatusObserving() requestLocalWeather() } updateRealtimeInteractionVisibility(isCurrentUser: isCurrentUser) @@ -84,6 +92,14 @@ class GroupMemberView2: UIView { func updatePhoneUsage(_ phoneUsage: PhoneUsageTodayModel?, for userId: String) { guard currentMemberModel?.user_id == userId else { return } applyPhoneUsage(phoneUsage) + if Self.isCurrentUser(userId) { + applyLocalDeviceInfo() + } + } + + func refreshLocalDeviceInfoIfNeeded() { + guard isShowingCurrentUser else { return } + applyLocalDeviceInfo() } func applyEmojiEcho(name: String, for userId: String) { @@ -223,8 +239,13 @@ class GroupMemberView2: UIView { } private var isShowingCurrentUser: Bool { - guard let userId = currentMemberModel?.user_id, !userId.isEmpty else { return false } - return userId == AppContextManager.shared.userId + Self.isCurrentUser(currentMemberModel?.user_id) + } + + private static func isCurrentUser(_ userId: String?) -> Bool { + let memberId = userId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + let selfId = AppContextManager.shared.userId.trimmingCharacters(in: .whitespacesAndNewlines) + return !memberId.isEmpty && memberId == selfId } private var isShowingOfflineOther: Bool { @@ -270,6 +291,7 @@ class GroupMemberView2: UIView { self.phoneStatusObserver = nil } weatherRequestID = UUID() + pendingWeatherLocationKey = nil } private func applyLocalDeviceInfo() { @@ -287,31 +309,65 @@ class GroupMemberView2: UIView { } private func requestLocalWeather() { - let requestID = UUID() - weatherRequestID = requestID - applyUnknownWeather(text: "***") guard let latitude = Defaults[\.currentLatitude], let longitude = Defaults[\.currentLongitude] else { - applyUnknownWeather(text: "***") + if localWeatherSnapshot == nil { + applyUnknownWeather(text: "***") + } return } let location = CLLocation(latitude: latitude, longitude: longitude) guard CLLocationCoordinate2DIsValid(location.coordinate) else { - applyUnknownWeather(text: "***") + if localWeatherSnapshot == nil { + applyUnknownWeather(text: "***") + } return } + let locationKey = Self.weatherLocationKey(location.coordinate) + if let snapshot = localWeatherSnapshot { + applyWeather(snapshot) + if localWeatherSnapshotKey == locationKey, + let localWeatherUpdatedAt, + Date().timeIntervalSince(localWeatherUpdatedAt) < Self.weatherRefreshInterval { + return + } + } else { + applyUnknownWeather(text: "***") + } + guard pendingWeatherLocationKey != locationKey else { return } + + let requestID = UUID() + weatherRequestID = requestID + pendingWeatherLocationKey = locationKey MemberWeatherService.shared.weather(for: location) { [weak self] result in - guard let self, self.weatherRequestID == requestID, self.isShowingCurrentUser else { return } + guard let self, self.weatherRequestID == requestID else { return } + self.pendingWeatherLocationKey = nil + guard self.isShowingCurrentUser else { return } switch result { case .success(let snapshot): - self.weatherIcon.image = UIImage(named: snapshot.assetName) - self.weatherLab.text = snapshot.text + self.localWeatherSnapshot = snapshot + self.localWeatherSnapshotKey = locationKey + self.localWeatherUpdatedAt = Date() + self.applyWeather(snapshot) case .failure: - self.applyUnknownWeather(text: "***") + if self.localWeatherSnapshot == nil { + self.applyUnknownWeather(text: "***") + } } } } + private func applyWeather(_ snapshot: MemberWeatherSnapshot) { + weatherIcon.image = UIImage(named: snapshot.assetName) + weatherLab.text = snapshot.text + } + + private static func weatherLocationKey(_ coordinate: CLLocationCoordinate2D) -> String { + let latitude = Int((coordinate.latitude * 1_000).rounded()) + let longitude = Int((coordinate.longitude * 1_000).rounded()) + return "\(latitude):\(longitude)" + } + private func updateAppUsageVisibility(_ visible: Bool) { if visible { phoneReportBottomConstraint?.isActive = false @@ -345,8 +401,7 @@ class GroupMemberView2: UIView { return } let snapshot = MemberWeatherService.snapshot(reportedText: weather) - weatherIcon.image = UIImage(named: snapshot.assetName) - weatherLab.text = snapshot.text + applyWeather(snapshot) } private func applyUnknownWeather(text: String) { diff --git a/QuickLocation/Section/Home/HomeView2.swift b/QuickLocation/Section/Home/HomeView2.swift index 683749e5..6e86c2de 100644 --- a/QuickLocation/Section/Home/HomeView2.swift +++ b/QuickLocation/Section/Home/HomeView2.swift @@ -222,7 +222,7 @@ class HomeView2: UIView { .centerY() let label = UILabel() - label.text = "有权限未开启,数据可能会异常" + label.text = "定位权限未开启,数据可能会异常" label.textColor = UIColor(hexStr: "#353B4F") label.font = .systemFont(ofSize: 12, weight: .medium) view.addSubview(label) diff --git a/QuickLocation/Section/Home/HomeViewController.swift b/QuickLocation/Section/Home/HomeViewController.swift index 80fb716e..09a18d24 100644 --- a/QuickLocation/Section/Home/HomeViewController.swift +++ b/QuickLocation/Section/Home/HomeViewController.swift @@ -126,6 +126,7 @@ class HomeViewController: BaseViewController { super.viewDidAppear(animated) UnlockCountManager.shared.refreshAuthorizationAndMonitoring() MQTTService.shared.applicationDidBecomeActive() + rootView.groupMemberView.refreshLocalDeviceInfoIfNeeded() let manager = AuthorizeManager.manager(type: .locationAlways) rootView.warningView.isHidden = manager?.authorizeStatus() == .authorized @@ -353,11 +354,11 @@ class HomeViewController: BaseViewController { self.openAppUsageReport() }).disposed(by: disposeBag) - rootView.groupMemberView.onPhoneReportRemind = { - DLToast.show(text: "已提醒") + rootView.groupMemberView.onPhoneReportRemind = { [weak self] in + self?.remindSelectedMember(content: "请开启手机报告隐私设置") } - rootView.groupMemberView.onAppUsageRemind = { - DLToast.show(text: "已提醒") + rootView.groupMemberView.onAppUsageRemind = { [weak self] in + self?.remindSelectedMember(content: "请开启App使用记录隐私设置") } // 地图回到自己并重新开启自动跟随 @@ -1150,6 +1151,14 @@ class HomeViewController: BaseViewController { } #endif + private func remindSelectedMember(content: String) { + GroupService.sendPrivilegeRemind( + content: content, + groupKey: viewModel.groupModel?.default_group_key ?? "", + userId: selectedMemberId + ) + } + /// 若更新的是当前选中成员,刷新信息卡(位置 / 在线 / 距离等) private func refreshSelectedMemberInfoIfNeeded(userId: String) { guard userId == selectedMemberId, @@ -1537,6 +1546,13 @@ extension HomeViewController { self?.removeAnnotation(userId: userId) } + case "bubblequit": + let targetId = msg.data?.user_id ?? userId + guard !targetId.isEmpty else { return } + DispatchQueue.main.async { [weak self] in + self?.handleBubbleQuit(userId: targetId) + } + case "sos": // 求助 guard let userId = msg.data?.user_id, userId != AppContextManager.shared.userId, @@ -1547,6 +1563,13 @@ extension HomeViewController { guard let member = self.viewModel.memberList.first(where: { $0.user_id == userId }) else { return } self.showSOSReceiveAlert(for: member) } + + case "sosquit": + let targetId = msg.data?.user_id ?? userId + guard !targetId.isEmpty else { return } + DispatchQueue.main.async { [weak self] in + self?.handleSOSQuit(userId: targetId) + } case "join", "leave", "dismiss", "kick": break @@ -1555,6 +1578,40 @@ extension HomeViewController { } } + private func handleSOSQuit(userId: String) { + viewModel.updateMemberStatus(userId: userId, status: 0) + updateSelectedMemberSOSGradient() + refreshSelectedMemberInfoIfNeeded(userId: userId) + guard let alertView = sosReceiveAlertView, alertView.superview != nil else { return } + if alertView.removeMember(userId: userId) { return } + alertView.dismiss() + if sosReceiveAlertView === alertView { + sosReceiveAlertView = nil + } + stopSOSAlarm() + } + + private func handleBubbleQuit(userId: String) { + viewModel.updateMemberStatus(userId: userId, status: 0) + refreshSelectedMemberInfoIfNeeded(userId: userId) + syncMemberAnnotations(viewModel.memberList) + if viewModel.isCurrentUser(id: userId) { + if let coord = trueCoordinateByUserId["current"] ?? trueCoordinateByUserId[userId], + CLLocationCoordinate2DIsValid(coord) { + updateAnnotation( + userId: "current", + coordinate: coord, + address: currentUserMember?.address ?? "" + ) + } + return + } + if let coord = trueCoordinateByUserId[userId], CLLocationCoordinate2DIsValid(coord) { + let address = viewModel.memberList.first(where: { $0.user_id == userId })?.lastLocation ?? "" + updateAnnotation(userId: userId, coordinate: coord, address: address) + } + } + /// 更新成员在线计数显示 private func updateOnlineCount() { let total = viewModel.memberCount diff --git a/QuickLocation/Section/Home/HomeViewModel.swift b/QuickLocation/Section/Home/HomeViewModel.swift index 9bd761fd..71411cd9 100644 --- a/QuickLocation/Section/Home/HomeViewModel.swift +++ b/QuickLocation/Section/Home/HomeViewModel.swift @@ -71,7 +71,9 @@ class HomeViewModel { // 是否当前用户 func isCurrentUser(id: String) -> Bool { - id == AppContextManager.shared.userId + let memberId = id.trimmingCharacters(in: .whitespacesAndNewlines) + let selfId = AppContextManager.shared.userId.trimmingCharacters(in: .whitespacesAndNewlines) + return !memberId.isEmpty && memberId == selfId } // 是否圈主 diff --git a/QuickLocation/Section/Home/InteractionView.swift b/QuickLocation/Section/Home/InteractionView.swift index e9157017..652c3b60 100644 --- a/QuickLocation/Section/Home/InteractionView.swift +++ b/QuickLocation/Section/Home/InteractionView.swift @@ -612,7 +612,7 @@ final class EmojiPanelCell: UICollectionViewCell { self.lottieView.play() } - lockView.isHidden = AppContextManager.shared.vip > 1 + lockView.isHidden = true//AppContextManager.shared.vip > 1 } func playAnimation() { diff --git a/QuickLocation/Section/Home/LocationPermissionPopView.swift b/QuickLocation/Section/Home/LocationPermissionPopView.swift index 9e21d134..89d59a48 100644 --- a/QuickLocation/Section/Home/LocationPermissionPopView.swift +++ b/QuickLocation/Section/Home/LocationPermissionPopView.swift @@ -75,7 +75,7 @@ class LocationPermissionPopView: UIView { lazy var popBgView: UIView = { let view = UIView() view.backgroundColor = .white - view.cornerRadius = 30 + view.cornerRadius = 40 return view }() diff --git a/QuickLocation/Section/Home/MemberAppUsageView.swift b/QuickLocation/Section/Home/MemberAppUsageView.swift index da850540..cf31fc0f 100644 --- a/QuickLocation/Section/Home/MemberAppUsageView.swift +++ b/QuickLocation/Section/Home/MemberAppUsageView.swift @@ -93,7 +93,8 @@ final class MemberAppUsageView: UIView { deniedOverlay.configure( title: "TA未开启设置", message: "无法查看app使用记录", - actionTitle: "提醒TA" + actionTitle: "提醒TA", + imageName: "PrivilegeDenied/app_usage" ) deniedOverlay.onAction = { [weak self] in self?.onDeniedAction?() } deniedOverlay.setVisible(denied) diff --git a/QuickLocation/Section/Home/MemberPhoneReportView.swift b/QuickLocation/Section/Home/MemberPhoneReportView.swift index 02b7d5d2..d58b44ad 100644 --- a/QuickLocation/Section/Home/MemberPhoneReportView.swift +++ b/QuickLocation/Section/Home/MemberPhoneReportView.swift @@ -71,7 +71,8 @@ final class MemberPhoneReportView: UIView { deniedOverlay.configure( title: "TA未开启设置", message: "无法查看手机报告", - actionTitle: "提醒TA" + actionTitle: "提醒TA", + imageName: "PrivilegeDenied/phone_report" ) deniedOverlay.onAction = { [weak self] in self?.onDeniedAction?() } deniedOverlay.setVisible(denied) diff --git a/QuickLocation/Section/Home/PhoneReportDetail/PhoneReportDetailVC.swift b/QuickLocation/Section/Home/PhoneReportDetail/PhoneReportDetailVC.swift index be0cd0c2..f2234491 100644 --- a/QuickLocation/Section/Home/PhoneReportDetail/PhoneReportDetailVC.swift +++ b/QuickLocation/Section/Home/PhoneReportDetail/PhoneReportDetailVC.swift @@ -53,6 +53,9 @@ final class PhoneReportDetailVC: BaseViewController { rootView.memberCV.delegate = self bind() rootView.memberCV.reloadData() + DispatchQueue.main.async { [weak self] in + self?.rootView.updateMemberFadeVisibility() + } } private func bind() { @@ -80,6 +83,30 @@ final class PhoneReportDetailVC: BaseViewController { }) .disposed(by: disposeBag) + viewModel.selectedMemberId + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] _ in + self?.updatePrivilegeOverlay() + }) + .disposed(by: disposeBag) + + rootView.onRemind = { [weak self] in + guard let self else { return } + GroupService.sendPrivilegeRemind( + content: "请开启手机报告隐私设置", + groupKey: self.groupModel?.default_group_key ?? "", + userId: self.viewModel.selectedMemberId.value + ) + } + updatePrivilegeOverlay() + } + + private func updatePrivilegeOverlay() { + let userId = viewModel.selectedMemberId.value + let isSelf = userId == AppContextManager.shared.userId + let member = viewModel.members.first { $0.user_id == userId } + let denied = !isSelf && member?.extra.privilege.phone_report == false + rootView.setPrivilegeDenied(denied) } private func switchGroup() { @@ -114,4 +141,9 @@ extension PhoneReportDetailVC: UICollectionViewDataSource, UICollectionViewDeleg let model = viewModel.members[indexPath.item] viewModel.selectedMemberId.accept(model.user_id) } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + guard scrollView === rootView.memberCV else { return } + rootView.updateMemberFadeVisibility() + } } diff --git a/QuickLocation/Section/Home/PhoneReportDetail/PhoneReportDetailView.swift b/QuickLocation/Section/Home/PhoneReportDetail/PhoneReportDetailView.swift index 012e884b..e6cf5991 100644 --- a/QuickLocation/Section/Home/PhoneReportDetail/PhoneReportDetailView.swift +++ b/QuickLocation/Section/Home/PhoneReportDetail/PhoneReportDetailView.swift @@ -30,23 +30,24 @@ final class PhoneReportDetailView: UIView { btn.extendEdgeInsets = UIEdgeInsets(top: 20, left: 20, bottom: 15, right: 18) return btn }() - + lazy var memberCV: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal layout.itemSize = CGSize(width: 61, height: 90) layout.minimumLineSpacing = 12 let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) - cv.backgroundColor = .white + cv.backgroundColor = .clear cv.showsHorizontalScrollIndicator = false - cv.contentInset = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12) + cv.contentInset = UIEdgeInsets(top: 5, left: 5, bottom: 0, right: 5) cv.register(GroupMemberListCell.self) - cv.applyPhoneReportCardStyle(cornerRadius: 20) +// cv.applyPhoneReportCardStyle(cornerRadius: 20) return cv }() let scrollView = UIScrollView() let contentView = UIView() + private let memberTrailingFadeView = PhoneReportMemberTrailingFadeView() // MARK: Status let statusTitleIcon = UIImageView(image: UIImage(named: "Home/report_phone")) @@ -65,6 +66,8 @@ final class PhoneReportDetailView: UIView { let deviceImage = UIImageView(image: UIImage(named: "Home/phone_device")) let brandLogoImage = UIImageView() private var brandLogoURL = "" + private let statusDeniedOverlay = PrivilegeDeniedOverlay() + var onRemind: (() -> Void)? // MARK: Charts let screenSection = PhoneReportChartSectionView( @@ -74,7 +77,8 @@ final class PhoneReportDetailView: UIView { summaryColor: UIColor(hexStr: "#00ADFE"), barGradient: (UIColor(hexStr: "#C0EAFF"), UIColor(hexStr: "#ECF9FF")), highlightGradient: (UIColor(hexStr: "#16B3FF"), UIColor(hexStr: "#9EDFFF")), - tipColor: UIColor(hexStr: "#20B6FF") + tipColor: UIColor(hexStr: "#20B6FF"), + tipArrowImageName: "Home/report_screen_tip_arrow" ) let unlockSection = PhoneReportChartSectionView( title: "APP使用次数", @@ -84,6 +88,7 @@ final class PhoneReportDetailView: UIView { barGradient: (UIColor(hexStr: "#DDFFA1"), UIColor(hexStr: "#F2FFDC")), highlightGradient: (UIColor(hexStr: "#AAF52A"), UIColor(hexStr: "#E8FFC1")), tipColor: UIColor(hexStr: "#86E73D"), + tipArrowImageName: "Home/report_usage_tip_arrow", valueSuffix: "次" ) @@ -118,11 +123,28 @@ final class PhoneReportDetailView: UIView { scrollView.addSubview(contentView) contentView.layoutChain.edges().widthToView(scrollView) - contentView.addSubview(memberCV) - memberCV.layoutChain + let memberView = UIView() + memberView.backgroundColor = .white + memberView.applyPhoneReportCardStyle(cornerRadius: 20) + + contentView.addSubview(memberView) + memberView.addSubview(memberCV) + memberView.addSubview(memberTrailingFadeView) + + memberView.layoutChain .top(8) .edgesHorzontal(15) .height(100) + + memberCV.layoutChain + .edgesVertical() + .edgesHorzontal() + + memberTrailingFadeView.layoutChain + .top() + .right() + .bottom() + .width(52) statusTitleIcon.contentMode = .scaleAspectFit contentView.addSubview(statusTitleIcon) @@ -147,6 +169,9 @@ final class PhoneReportDetailView: UIView { .topToBottomOfView(statusTitleIcon, offset: 10) .edgesHorzontal(15) .height(175) + statusCard.addSubview(statusDeniedOverlay) + statusDeniedOverlay.layoutChain.edges() + statusDeniedOverlay.layer.cornerRadius = 20 let leftPanel = UIView() leftPanel.backgroundColor = .clear @@ -182,7 +207,7 @@ final class PhoneReportDetailView: UIView { .right(12) .height(64) - deviceCard.backgroundColor = UIColor(hexStr: "#F7FBFF") + deviceCard.backgroundColor = UIColor(hexStr: "#FAFAFA") deviceCard.cornerRadius = 12 statusCard.addSubview(deviceCard) deviceCard.layoutChain @@ -240,6 +265,21 @@ final class PhoneReportDetailView: UIView { .bottom(24) } + override func layoutSubviews() { + super.layoutSubviews() + updateMemberFadeVisibility() + } + + func updateMemberFadeVisibility() { + let rightInset = memberCV.adjustedContentInset.right + let maxOffsetX = max( + -memberCV.adjustedContentInset.left, + memberCV.contentSize.width - memberCV.bounds.width + rightInset + ) + memberTrailingFadeView.isHidden = memberCV.contentOffset.x >= maxOffsetX - 1 + || memberCV.contentSize.width <= memberCV.bounds.width + } + func apply(_ snap: PhoneReportDetailViewModel.StatusSnapshot) { batteryRing.configure(percent: snap.batteryPercent, text: snap.batteryText, isCharging: snap.isCharging) estimateLab.text = "预计使用\(snap.estimateText)" @@ -256,6 +296,27 @@ final class PhoneReportDetailView: UIView { unlockSection.setBars(snap.appUsageBars) } + func setPrivilegeDenied(_ denied: Bool) { + let title = "TA未开启设置" + let message = "无法查看手机状态" + let action = "提醒TA" + statusDeniedOverlay.configure( + title: title, + message: message, + actionTitle: action, + imageName: "PrivilegeDenied/report_status" + ) + statusDeniedOverlay.onAction = { [weak self] in self?.onRemind?() } + statusDeniedOverlay.setVisible(denied) + if denied { statusCard.bringSubviewToFront(statusDeniedOverlay) } + screenSection.setDenied(denied, imageName: "PrivilegeDenied/report_screen", onRemind: { [weak self] in + self?.onRemind?() + }) + unlockSection.setDenied(denied, imageName: "PrivilegeDenied/report_usage", onRemind: { [weak self] in + self?.onRemind?() + }) + } + private func updateBrandLogo(urlString: String) { brandLogoURL = urlString brandLogoImage.kf.cancelDownloadTask() @@ -284,10 +345,10 @@ private extension UIView { layer.cornerRadius = cornerRadius clipsToBounds = false layer.masksToBounds = false - layer.shadowColor = UIColor(hexStr: "#0F2846", alpha: 0.1).cgColor - layer.shadowOffset = CGSize(width: 0, height: 2) + layer.shadowColor = UIColor(hexStr: "#E5E5E5", alpha: 0.3).cgColor + layer.shadowOffset = CGSize(width: 0, height: 0) layer.shadowOpacity = 1 - layer.shadowRadius = 10 + layer.shadowRadius = 5 } } @@ -351,7 +412,7 @@ final class PhoneStatusMiniCard: UIView { init(iconName: String, title: String) { super.init(frame: .zero) - backgroundColor = UIColor(hexStr: "#F7FBFF") + backgroundColor = UIColor(hexStr: "#FAFAFA") cornerRadius = 12 icon.image = UIImage(named: iconName) icon.contentMode = .scaleAspectFit @@ -387,7 +448,10 @@ final class PhoneReportChartSectionView: UIView { private let barGradient: (UIColor, UIColor) private let highlightGradient: (UIColor, UIColor) private let tipColor: UIColor + private let tipArrowImageName: String private let valueSuffix: String + private let card = UIView() + private let deniedOverlay = PrivilegeDeniedOverlay() init( title: String, @@ -397,6 +461,7 @@ final class PhoneReportChartSectionView: UIView { barGradient: (UIColor, UIColor), highlightGradient: (UIColor, UIColor), tipColor: UIColor, + tipArrowImageName: String, valueSuffix: String = "" ) { self.summaryPrefix = summaryPrefix @@ -404,6 +469,7 @@ final class PhoneReportChartSectionView: UIView { self.barGradient = barGradient self.highlightGradient = highlightGradient self.tipColor = tipColor + self.tipArrowImageName = tipArrowImageName self.valueSuffix = valueSuffix super.init(frame: .zero) let titleIcon = UIImageView(image: UIImage(named: iconName)) @@ -416,11 +482,13 @@ final class PhoneReportChartSectionView: UIView { addSubview(titleLab) titleLab.layoutChain.centerY(titleIcon).leftToRightOfView(titleIcon, offset: 6) - let card = UIView() card.backgroundColor = .white card.applyPhoneReportCardStyle(cornerRadius: 20) addSubview(card) card.layoutChain.topToBottomOfView(titleLab, offset: 10).edgesHorzontal().bottom().height(180) + card.addSubview(deniedOverlay) + deniedOverlay.layoutChain.edges() + deniedOverlay.layer.cornerRadius = 20 summaryPrefixLab.text = summaryPrefix summaryPrefixLab.font = .systemFont(ofSize: 12, weight: .medium) @@ -436,7 +504,7 @@ final class PhoneReportChartSectionView: UIView { summaryLab.layoutChain .centerY(summaryPrefixLab) .leftToRightOfView(summaryPrefixLab, offset: 4) - .right(14, relation: .lessThanOrEqual) + .right(14, relation: .greaterThanOrEqual) card.addSubview(chart) chart.layoutChain.topToBottomOfView(summaryPrefixLab, offset: 16).edgesHorzontal(25).bottom(14) @@ -444,6 +512,18 @@ final class PhoneReportChartSectionView: UIView { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + func setDenied(_ denied: Bool, imageName: String, onRemind: (() -> Void)?) { + deniedOverlay.configure( + title: "TA未开启设置", + message: "无法查看手机状态", + actionTitle: "提醒TA", + imageName: imageName + ) + deniedOverlay.onAction = onRemind + deniedOverlay.setVisible(denied) + if denied { card.bringSubviewToFront(deniedOverlay) } + } + func setSummary(_ text: String) { summaryLab.text = text } @@ -454,6 +534,7 @@ final class PhoneReportChartSectionView: UIView { barGradient: barGradient, highlightGradient: highlightGradient, tipColor: tipColor, + tipArrowImageName: tipArrowImageName, suffix: valueSuffix ) } @@ -472,6 +553,7 @@ final class PhoneReportBarChartView: UIView { private var barSpacing: CGFloat = 0 private var baselineY: CGFloat = 0 private let tipLab = UILabel() + private let tipArrowView = UIImageView() private let baseline = UIView() override init(frame: CGRect) { @@ -488,6 +570,10 @@ final class PhoneReportBarChartView: UIView { tipLab.isHidden = true tipLab.isUserInteractionEnabled = false addSubview(tipLab) + tipArrowView.contentMode = .scaleAspectFit + tipArrowView.isHidden = true + tipArrowView.isUserInteractionEnabled = false + addSubview(tipArrowView) let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))) addGestureRecognizer(tap) @@ -500,6 +586,7 @@ final class PhoneReportBarChartView: UIView { barGradient: (UIColor, UIColor), highlightGradient: (UIColor, UIColor), tipColor: UIColor, + tipArrowImageName: String, suffix: String ) { let previous = self.bars @@ -507,6 +594,7 @@ final class PhoneReportBarChartView: UIView { self.barGradient = barGradient self.highlightGradient = highlightGradient self.tipColor = tipColor + tipArrowView.image = UIImage(named: tipArrowImageName) self.suffix = suffix let dataChanged = previous.count != bars.count || zip(previous, bars).contains { $0.label != $1.label || $0.value != $1.value } @@ -540,6 +628,7 @@ final class PhoneReportBarChartView: UIView { labelViews.removeAll() barFrames.removeAll() tipLab.isHidden = true + tipArrowView.isHidden = true guard !bars.isEmpty else { baseline.isHidden = true return @@ -549,7 +638,11 @@ final class PhoneReportBarChartView: UIView { let maxVal = max(bars.map(\.value).max() ?? 1, 1) let count = bars.count let labelH: CGFloat = 16 - let chartH = bounds.height - labelH - 24 + let tipVerticalPadding: CGFloat = 8 + let tipArrowHeight: CGFloat = 5 + let tipArrowBarSpacing: CGFloat = 6 + let tipHeight = ceil(tipLab.font.lineHeight) + tipVerticalPadding + let chartH = max(4, bounds.height - labelH - tipHeight - tipArrowHeight - tipArrowBarSpacing) let barW: CGFloat = 16 barSpacing = count > 1 ? max(0, (bounds.width - barW * CGFloat(count)) / CGFloat(count - 1)) @@ -577,6 +670,7 @@ final class PhoneReportBarChartView: UIView { gradient.startPoint = CGPoint(x: 0.5, y: 0) gradient.endPoint = CGPoint(x: 0.5, y: 1) gradient.cornerRadius = 4 + gradient.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] layer.insertSublayer(gradient, below: tipLab.layer) barLayers.append(gradient) @@ -594,18 +688,25 @@ final class PhoneReportBarChartView: UIView { tipLab.backgroundColor = tipColor tipLab.sizeToFit() let tipW = tipLab.bounds.width + 5 + 5 - let tipH = tipLab.bounds.height + 4 + 4 // 与柱子水平居中,允许超出 chart 左右边界 tipLab.frame = CGRect( x: x + barW / 2 - tipW / 2, - y: max(0, y - tipH - 4), + y: max(0, y - tipHeight - tipArrowHeight - tipArrowBarSpacing), width: tipW, - height: tipH + height: tipHeight ) tipLab.isHidden = false + tipArrowView.frame = CGRect( + x: x + barW / 2 - 5, + y: tipLab.frame.maxY, + width: 10, + height: tipArrowHeight + ) + tipArrowView.isHidden = false } } bringSubviewToFront(tipLab) + bringSubviewToFront(tipArrowView) } private func tipText(for bar: PhoneReportDetailViewModel.ChartBar) -> String { @@ -620,3 +721,27 @@ final class PhoneReportBarChartView: UIView { return "\(mins)分钟" } } + +private final class PhoneReportMemberTrailingFadeView: UIView { + override class var layerClass: AnyClass { CAGradientLayer.self } + + override init(frame: CGRect) { + super.init(frame: frame) + isUserInteractionEnabled = false + let gradientLayer = layer as! CAGradientLayer + gradientLayer.colors = [ + UIColor.white.withAlphaComponent(0).cgColor, + UIColor.white.withAlphaComponent(0.88).cgColor, + UIColor.white.cgColor + ] + gradientLayer.locations = [0, 0.7, 1] + gradientLayer.startPoint = CGPoint(x: 0, y: 0.5) + gradientLayer.endPoint = CGPoint(x: 1, y: 0.5) + gradientLayer.cornerRadius = 20 + gradientLayer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner] + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/QuickLocation/Section/Home/PhoneReportDetail/PhoneReportDetailViewModel.swift b/QuickLocation/Section/Home/PhoneReportDetail/PhoneReportDetailViewModel.swift index 86b8f8dc..f916b312 100644 --- a/QuickLocation/Section/Home/PhoneReportDetail/PhoneReportDetailViewModel.swift +++ b/QuickLocation/Section/Home/PhoneReportDetail/PhoneReportDetailViewModel.swift @@ -144,7 +144,10 @@ final class PhoneReportDetailViewModel { batteryText: local.batteryPercent == nil ? placeholder : local.batteryText, batteryPercent: local.batteryPercent, isCharging: local.isCharging, - estimateText: placeholder, + estimateText: Self.estimatedUsageText( + batteryPercent: local.batteryPercent, + placeholder: placeholder + ), brightnessText: local.brightnessText, volumeText: local.volumeText, wifiText: local.network, @@ -162,11 +165,15 @@ final class PhoneReportDetailViewModel { private func makeRemoteSnapshot(report: PhoneUsageReportModel) -> StatusSnapshot { let info = report.phoneInfo let charts = chartFields(from: report) + let batteryPercent = Self.percent(from: info?.battery) return StatusSnapshot( batteryText: Self.displayText(info?.battery, placeholder: placeholder), - batteryPercent: Self.percent(from: info?.battery), + batteryPercent: batteryPercent, isCharging: false, - estimateText: placeholder, + estimateText: Self.estimatedUsageText( + batteryPercent: batteryPercent, + placeholder: placeholder + ), brightnessText: Self.displayText(info?.brightness, placeholder: placeholder), volumeText: Self.displayText(info?.volume, placeholder: placeholder), wifiText: Self.displayText(info?.network, placeholder: placeholder), @@ -287,6 +294,13 @@ final class PhoneReportDetailViewModel { return max(0, min(100, Int(number.rounded()))) } + private static func estimatedUsageText(batteryPercent: Int?, placeholder: String) -> String { + guard let batteryPercent else { return placeholder } + let estimatedMinutes = batteryPercent * 9 + let hours = max(1, Int((Double(estimatedMinutes) / 60).rounded())) + return "\(hours)小时" + } + private static func storageRatio(used: String?, total: String?) -> CGFloat { guard let usedValue = firstNumber(in: used), let totalValue = firstNumber(in: total), diff --git a/QuickLocation/Section/Home/ReceiveMessagePopView.swift b/QuickLocation/Section/Home/ReceiveMessagePopView.swift index 25c7af15..c6cd7c0d 100644 --- a/QuickLocation/Section/Home/ReceiveMessagePopView.swift +++ b/QuickLocation/Section/Home/ReceiveMessagePopView.swift @@ -375,7 +375,11 @@ final class ReceiveMessagePopView: UIView { avatarView.setHeadPic(item.headPic) messageImageView.kf.cancelDownloadTask() if let imageURL = item.imageURL { - messageImageView.kf.setImage(with: imageURL, placeholder: item.image) + messageImageView.kf.setImage( + with: imageURL, + placeholder: item.image, + options: [.onFailureImage(.brokenNetworkImage)] + ) } else { messageImageView.image = item.image } diff --git a/QuickLocation/Section/Home/ReceiveMessageReportPopView.swift b/QuickLocation/Section/Home/ReceiveMessageReportPopView.swift index f1a5762a..dd59608b 100644 --- a/QuickLocation/Section/Home/ReceiveMessageReportPopView.swift +++ b/QuickLocation/Section/Home/ReceiveMessageReportPopView.swift @@ -11,6 +11,7 @@ final class ReceiveMessageReportPopView: UIView { private static let shared = ReceiveMessageReportPopView( frame: CGRect(origin: .zero, size: kScreenSize) ) + private static let defaultCenterYOffset: CGFloat = 8 private let reasons = ["诈骗", "涉黄", "违规行为", "散播不良广告", "谩骂攻击", "其他"] private var selectedReasons = Set() @@ -52,17 +53,26 @@ final class ReceiveMessageReportPopView: UIView { return view }() - private let popView = UIView() + private let popView: UIView = { + let view = UIView() + view.backgroundColor = .clear + view.clipsToBounds = false + return view + }() private let popBgView: UIView = { let view = UIView() view.backgroundColor = .white - view.layer.cornerRadius = 30 + view.layer.cornerRadius = 40 view.clipsToBounds = true return view }() - private let logoImgView = UIImageView(image: UIImage(named: "Popup/bell")) + private let logoImgView: UIImageView = { + let view = UIImageView(image: UIImage(named: "LockDistract/horn_bird")) + view.contentMode = .scaleAspectFit + return view + }() private let headerBgImg: UIImageView = { let view = UIImageView(image: UIImage(named: "Popup/header_bg")) @@ -83,9 +93,9 @@ final class ReceiveMessageReportPopView: UIView { private let optionsStack: UIStackView = { let stack = UIStackView() stack.axis = .vertical - stack.spacing = 22 + stack.spacing = 7 stack.alignment = .fill - stack.distribution = .fillEqually + stack.distribution = .fill return stack }() @@ -175,24 +185,32 @@ final class ReceiveMessageReportPopView: UIView { popView.layoutChain .centerX() .width(315) + .height(450) - popCenterYConstraint = popView.centerYAnchor.constraint(equalTo: centerYAnchor) + popCenterYConstraint = popView.centerYAnchor.constraint( + equalTo: centerYAnchor, + constant: Self.defaultCenterYOffset + ) popCenterYConstraint?.isActive = true - popBgView.layoutChain.edges() + popBgView.layoutChain + .top(30) + .left() + .right() + .bottom() logoImgView.layoutChain - .left(16) - .top(-45) - .width(109) - .height(109) + .left(42) + .top() + .width(100) + .height(74) headerBgImg.layoutChain .edges(excludingEdge: .bottom) .heightToWidth(100 / 315) titleLabel.layoutChain - .top(70) + .top(48) .edgesHorzontal(20) optionViews = reasons.map { reason in @@ -202,30 +220,31 @@ final class ReceiveMessageReportPopView: UIView { let rowStack = UIStackView() rowStack.axis = .horizontal rowStack.alignment = .fill - rowStack.distribution = .fillEqually - rowStack.spacing = 8 + rowStack.distribution = .fill + rowStack.spacing = 0 rowStack.isUserInteractionEnabled = true rowStack.addArrangedSubview(optionViews[row * 2]) rowStack.addArrangedSubview(optionViews[row * 2 + 1]) + optionViews[row * 2].layoutChain.width(118) optionsStack.addArrangedSubview(rowStack) rowStack.layoutChain.height(36) } optionsStack.isUserInteractionEnabled = true optionsStack.layoutChain - .topToBottomOfView(titleLabel, offset: 24) - .left(24) - .right(20) + .topToBottomOfView(titleLabel, offset: 18) + .left(32) + .right(18) popBgView.bringSubviewToFront(optionsStack) popBgView.bringSubviewToFront(inputContainer) popBgView.bringSubviewToFront(confirmBtn) popBgView.bringSubviewToFront(cancelBtn) inputContainer.layoutChain - .topToBottomOfView(optionsStack, offset: 16) - .left(20) - .right(20) - .height(86) + .topToBottomOfView(optionsStack, offset: 10) + .left(30) + .right(24) + .height(80) remarkTextView.layoutChain.edges() placeholderLabel.layoutChain @@ -244,7 +263,7 @@ final class ReceiveMessageReportPopView: UIView { .left(20) .right(20) .height(50) - .bottom(8) + .bottom() } private func setupActions() { @@ -266,7 +285,7 @@ final class ReceiveMessageReportPopView: UIView { remarkTextView.text = "" placeholderLabel.isHidden = false optionViews.forEach { $0.isChosen = false } - popCenterYConstraint?.constant = 0 + popCenterYConstraint?.constant = Self.defaultCenterYOffset updateConfirmState() } @@ -342,7 +361,9 @@ final class ReceiveMessageReportPopView: UIView { let keyboardFrame = convert(frame, from: nil) let inputFrame = inputContainer.convert(inputContainer.bounds, to: self) let overlap = inputFrame.maxY + 16 - keyboardFrame.minY - popCenterYConstraint?.constant = overlap > 0 ? -overlap : 0 + popCenterYConstraint?.constant = overlap > 0 + ? Self.defaultCenterYOffset - overlap + : Self.defaultCenterYOffset UIView.animate(withDuration: duration) { self.layoutIfNeeded() } @@ -350,7 +371,7 @@ final class ReceiveMessageReportPopView: UIView { @objc private func keyboardWillHide(_ notification: Notification) { let duration = (notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double) ?? 0.25 - popCenterYConstraint?.constant = 0 + popCenterYConstraint?.constant = Self.defaultCenterYOffset UIView.animate(withDuration: duration) { self.layoutIfNeeded() } @@ -432,8 +453,8 @@ final class ReceiveMessageReportOptionView: UIButton { boxView.layoutChain .left() .centerY() - .width(22) - .height(22) + .width(20) + .height(20) reasonLabel.layoutChain .leftToRightOfView(boxView, offset: 8) @@ -455,13 +476,13 @@ final class ReceiveMessageReportOptionView: UIButton { fatalError("init(coder:) has not been implemented") } - private static let boxSize = CGSize(width: 22, height: 22) + private static let boxSize = CGSize(width: 20, height: 20) private static let normalImage = UIGraphicsImageRenderer(size: boxSize).image { _ in let rect = CGRect(origin: .zero, size: boxSize).insetBy(dx: 1, dy: 1) UIColor.white.setFill() UIBezierPath(ovalIn: rect).fill() - UIColor(hexStr: "#D0D5DD").setStroke() + UIColor(hexStr: "#C5C7CA").setStroke() let border = UIBezierPath(ovalIn: rect) border.lineWidth = 1.5 border.stroke() @@ -476,9 +497,9 @@ final class ReceiveMessageReportOptionView: UIButton { check.lineWidth = 1.8 check.lineCapStyle = .round check.lineJoinStyle = .round - check.move(to: CGPoint(x: 6.2, y: 11.4)) - check.addLine(to: CGPoint(x: 9.4, y: 14.6)) - check.addLine(to: CGPoint(x: 16.0, y: 7.8)) + check.move(to: CGPoint(x: 5.5, y: 10.4)) + check.addLine(to: CGPoint(x: 8.6, y: 13.5)) + check.addLine(to: CGPoint(x: 14.8, y: 7.2)) UIColor.white.setStroke() check.stroke() } diff --git a/QuickLocation/Section/Home/SOS/SOSReceiveAlertView.swift b/QuickLocation/Section/Home/SOS/SOSReceiveAlertView.swift index e42c3b55..474ac7dd 100644 --- a/QuickLocation/Section/Home/SOS/SOSReceiveAlertView.swift +++ b/QuickLocation/Section/Home/SOS/SOSReceiveAlertView.swift @@ -179,6 +179,38 @@ final class SOSReceiveAlertView: UIView { setNeedsLayout() } + /// 该人取消 SOS。返回 `false` 表示弹层已无人,应关闭。 + @discardableResult + func removeMember(userId: String) -> Bool { + let trimmed = userId.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return true } + + if trimmed == primaryMemberID { + guard let next = secondaryMembers.first else { return false } + applyPrimary(next) + removeSecondary(userId: next.user_id) + return true + } + + if secondaryMembers.contains(where: { $0.user_id == trimmed }) { + removeSecondary(userId: trimmed) + } + return true + } + + private func applyPrimary(_ member: GroupMemberModel) { + primaryMemberID = member.user_id + primaryAvatarView.setHeadPic(member.displayHeadPic) + nameLabel.text = member.showName.isEmpty ? "圈子成员" : member.showName + } + + private func removeSecondary(userId: String) { + secondaryMembers.removeAll { $0.user_id == userId } + secondaryAvatarViews[userId]?.removeFromSuperview() + secondaryAvatarViews.removeValue(forKey: userId) + setNeedsLayout() + } + func dismiss() { stopAnimations() secondaryAvatarViews.values.forEach { $0.removeFromSuperview() } diff --git a/QuickLocation/Section/Home/SearchLocation/SearchLocationVC.swift b/QuickLocation/Section/Home/SearchLocation/SearchLocationVC.swift index cc9fdb08..52b45280 100644 --- a/QuickLocation/Section/Home/SearchLocation/SearchLocationVC.swift +++ b/QuickLocation/Section/Home/SearchLocation/SearchLocationVC.swift @@ -35,6 +35,7 @@ class SearchLocationVC: BaseViewController { DLToast.show(text: "请输入正确的手机号码") return } + self.rootView.phoneInputTF.resignFirstResponder() self.requestSearchPhone(phone: phone) self.rootView.searchProgressView.isHidden = false self.rootView.playVideo { diff --git a/QuickLocation/Section/LockDistract/LockDistractVC.swift b/QuickLocation/Section/LockDistract/LockDistractVC.swift index 78b6bcad..f755cb5a 100644 --- a/QuickLocation/Section/LockDistract/LockDistractVC.swift +++ b/QuickLocation/Section/LockDistract/LockDistractVC.swift @@ -72,7 +72,11 @@ final class LockDistractVC: BaseViewController { if self.selectedMember?.user_id == AppContextManager.shared.userId { AppRouter.push(Route.privacySettings) } else { - DLToast.show(text: "已提醒") + GroupService.sendPrivilegeRemind( + content: "请开启一键锁机隐私设置", + groupKey: self.activeGroupKey, + userId: self.selectedMember?.user_id ?? "" + ) } } diff --git a/QuickLocation/Section/LockDistract/LockDistractView.swift b/QuickLocation/Section/LockDistract/LockDistractView.swift index 1d2ab796..c3012dde 100644 --- a/QuickLocation/Section/LockDistract/LockDistractView.swift +++ b/QuickLocation/Section/LockDistract/LockDistractView.swift @@ -36,6 +36,9 @@ final class LockDistractView: UIView { private(set) var isCurrentUser = false private(set) var primaryAction: LockDistractPrimaryAction = .lock private let copyMaxLength = 20 + private var lockBtnHeightConstraint: NSLayoutConstraint? + private var bottomStackToAppsConstraint: NSLayoutConstraint? + private var bottomStackToStatsConstraint: NSLayoutConstraint? override init(frame: CGRect) { super.init(frame: frame) @@ -119,30 +122,51 @@ final class LockDistractView: UIView { var onPrivilegeAction: (() -> Void)? private(set) var isPrivilegeDenied = false - private let privilegeOverlay = PrivilegeDeniedOverlay() + private let appsPrivilegeOverlay = PrivilegeDeniedOverlay() + private let stylePrivilegeOverlay = PrivilegeDeniedOverlay() func setPrivilegeDenied(_ denied: Bool, isCurrentUser: Bool) { isPrivilegeDenied = denied - privilegeOverlay.configure( + appsPrivilegeOverlay.configure( + title: "", + message: "", + actionTitle: "", + imageName: "PrivilegeDenied/lock_apps" + ) + stylePrivilegeOverlay.configure( title: "TA未开启权限", message: "无法查看可锁应用", - actionTitle: isCurrentUser ? "去开启" : "提醒TA" + actionTitle: isCurrentUser ? "去开启" : "提醒TA", + imageName: "PrivilegeDenied/lock_style" ) - privilegeOverlay.onAction = { [weak self] in self?.onPrivilegeAction?() } - privilegeOverlay.setVisible(denied) + stylePrivilegeOverlay.onAction = { [weak self] in self?.onPrivilegeAction?() } + appsPrivilegeOverlay.setVisible(denied) + stylePrivilegeOverlay.setVisible(denied) if denied { - contentView.bringSubviewToFront(privilegeOverlay) + appsCard.bringSubviewToFront(appsPrivilegeOverlay) + styleCard.bringSubviewToFront(stylePrivilegeOverlay) } } private func applyLockedState() { + let lockedByOthers = primaryAction == .hidden updateLockedEmptyImage() avatarLockView.isHidden = !isLocked styleSectionTitle.isHidden = isLocked styleCard.isHidden = isLocked lockedEmptyView.isHidden = !isLocked + appsSectionTitle.isHidden = lockedByOthers + appsCard.isHidden = lockedByOthers + if lockedByOthers { + bottomStackToAppsConstraint?.isActive = false + bottomStackToStatsConstraint?.isActive = true + } else { + bottomStackToStatsConstraint?.isActive = false + bottomStackToAppsConstraint?.isActive = true + } appCollectionView.reloadData() - lockBtn.isHidden = primaryAction == .hidden + lockBtn.isHidden = lockedByOthers + lockBtnHeightConstraint?.constant = lockedByOthers ? 0 : 56 switch primaryAction { case .lock: tipLab.text = "锁定功能需要在对方开启权限情况下才能使用哟~" @@ -184,7 +208,8 @@ final class LockDistractView: UIView { bottomStack.addArrangedSubview(styleSectionTitle) bottomStack.addArrangedSubview(styleCard) bottomStack.addArrangedSubview(lockedEmptyView) - contentView.addSubview(privilegeOverlay) + appsCard.addSubview(appsPrivilegeOverlay) + styleCard.addSubview(stylePrivilegeOverlay) navView.layoutChain .edges(excludingEdge: .bottom) @@ -198,6 +223,7 @@ final class LockDistractView: UIView { .bottom(kSafeBottomMargin) .edgesHorzontal(30) .height(56) + lockBtnHeightConstraint = lockBtn.jh_lastConstraint tipRow.layoutChain .centerX() @@ -246,18 +272,23 @@ final class LockDistractView: UIView { .topToBottomOfView(appsCard, offset: 11) .edgesHorzontal(16) .bottom(20) + bottomStackToAppsConstraint = bottomStack.jh_constraint( + .top, toAttribute: .bottom, otherView: appsCard, relation: .equal + ) + bottomStackToStatsConstraint = bottomStack.topAnchor.constraint( + equalTo: statsRow.bottomAnchor, + constant: 36 + ) - privilegeOverlay.layoutChain - .topToView(appsSectionTitle) - .left(16) - .right(16) - .bottomToView(bottomStack) - privilegeOverlay.layer.cornerRadius = 26 + appsPrivilegeOverlay.layoutChain.edges() + appsPrivilegeOverlay.layer.cornerRadius = 26 + stylePrivilegeOverlay.layoutChain.edges() + stylePrivilegeOverlay.layer.cornerRadius = 26 styleSectionTitle.layoutChain.left(4) lockedEmptyView.layoutChain .centerX() -// .height(150) + .height(150) } private func setupCopyLimit() { diff --git a/QuickLocation/Section/Mine/MineView.swift b/QuickLocation/Section/Mine/MineView.swift index 2313432a..fd055ec9 100644 --- a/QuickLocation/Section/Mine/MineView.swift +++ b/QuickLocation/Section/Mine/MineView.swift @@ -57,10 +57,17 @@ final class MineView: UIView { private lazy var headerBgView: UIView = { let v = UIView() - v.backgroundColor = .clear + v.backgroundColor = UIColor(hexStr: "#F5FBFB") return v }() + lazy var navBgView: UIImageView = { + let iv = UIImageView() + iv.image = UIImage(named: "Common/navBar_bg_1") + iv.contentMode = .scaleAspectFill + return iv + }() + lazy var photoWall: MinePhotoWallView = { MinePhotoWallView() }() @@ -173,13 +180,14 @@ final class MineView: UIView { } private func setupUI() { - backgroundColor = UIColor(hexStr: "#FAFAFA") + backgroundColor = UIColor(hexStr: "#F5FBFB") addSubview(scrollView) scrollView.addSubview(contentView) contentView.addSubview(headerBgView) // 照片墙固定顶部,不随列表滚动 + addSubview(navBgView) addSubview(photoWall) contentView.addSubview(vipCardView) @@ -197,6 +205,18 @@ final class MineView: UIView { } private func setupLayout() { + headerBgView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + headerBgView.topAnchor.constraint(equalTo: contentView.topAnchor), + headerBgView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + headerBgView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + headerBgView.bottomAnchor.constraint(equalTo: quickCardView.topAnchor) + ]) + + navBgView.layoutChain + .edges(excludingEdge: .bottom) + .heightToWidth(160 / 375) + photoWall.layoutChain .top(40) .edgesHorzontal() @@ -310,14 +330,6 @@ final class MineView: UIView { override func layoutSubviews() { super.layoutSubviews() - // 顶部淡色渐变氛围 - setGradientLayer( - frame: CGRect(x: 0, y: 0, width: bounds.width, height: min(280, bounds.height * 0.35)), - startPoint: CGPoint(x: 0.5, y: 0), - endPoint: CGPoint(x: 0.5, y: 1), - colors: [UIColor(hexStr: "#D8F4FF"), UIColor(hexStr: "#FAFAFA")], - locations: [0, 1] - ) refreshTableViewHeight() } diff --git a/QuickLocation/Section/Mine/MoodStatus/MoodStatusVC.swift b/QuickLocation/Section/Mine/MoodStatus/MoodStatusVC.swift index a4e124dc..ba4f75f1 100644 --- a/QuickLocation/Section/Mine/MoodStatus/MoodStatusVC.swift +++ b/QuickLocation/Section/Mine/MoodStatus/MoodStatusVC.swift @@ -99,10 +99,9 @@ final class MoodStatusVC: BaseViewController { .subscribe(onNext: { [weak self] _ in self?.viewModel.endSaving() AppContextManager.shared.updateAccountMood(moodIndex) - DLToast.show(text: "保存成功") { - AppRouter.shared.popOrDismiss() - NotificationCenter.default.post(name: .RefreshGroupInfoNotification, object: nil) - } + self?.rootView.setSaving(false) + DLToast.show(text: "保存成功") + NotificationCenter.default.post(name: .RefreshGroupInfoNotification, object: nil) }, onError: { [weak self] _ in self?.viewModel.endSaving() self?.rootView.setSaving(false) diff --git a/QuickLocation/Section/Mine/MyProfile/AvatarIconListVC.swift b/QuickLocation/Section/Mine/MyProfile/AvatarIconListVC.swift index 3de9b53d..40723101 100644 --- a/QuickLocation/Section/Mine/MyProfile/AvatarIconListVC.swift +++ b/QuickLocation/Section/Mine/MyProfile/AvatarIconListVC.swift @@ -30,7 +30,6 @@ class AvatarIconListVC: BaseViewController { let selected = self.rootView.selectedHeadPic guard !selected.isEmpty else { return } self.onSelectIcon?(selected) - AppRouter.shared.popOrDismiss() }).disposed(by: disposeBag) rootView.onBadgeTap = { [weak self] in @@ -84,7 +83,6 @@ class AvatarIconListVC: BaseViewController { guard let self else { return } self.rootView.setPreviewImage(image) self.onSelectIcon?(url) - AppRouter.shared.popOrDismiss() }, onError: { error in DLToast.show(text: error.gatewayMessage ?? "更换失败") }) diff --git a/QuickLocation/Section/PigeonMessage/PigeonMessageHistoryView.swift b/QuickLocation/Section/PigeonMessage/PigeonMessageHistoryView.swift index 10e96f80..a1ae8083 100644 --- a/QuickLocation/Section/PigeonMessage/PigeonMessageHistoryView.swift +++ b/QuickLocation/Section/PigeonMessage/PigeonMessageHistoryView.swift @@ -215,7 +215,11 @@ final class PigeonHistoryCell: UITableViewCell { if let image = item.image, !isVoice { photoView.image = image } else if let mediaURL = item.mediaURL, !isVoice { - photoView.kf.setImage(with: mediaURL, placeholder: nil) + photoView.kf.setImage( + with: mediaURL, + placeholder: nil, + options: [.onFailureImage(.brokenNetworkImage)] + ) } else { photoView.image = nil } diff --git a/QuickLocation/Section/PigeonMessage/PigeonMessageView.swift b/QuickLocation/Section/PigeonMessage/PigeonMessageView.swift index 30f804a7..49949528 100644 --- a/QuickLocation/Section/PigeonMessage/PigeonMessageView.swift +++ b/QuickLocation/Section/PigeonMessage/PigeonMessageView.swift @@ -679,7 +679,7 @@ private extension PigeonMessageView { if selectedMemberIds.contains(id) { selectedMemberIds.remove(id) } else if !member.extra.privilege.pigeon_mail { - DLToast.show(text: "该用户未开启设置,无法接收飞鸽传书") + DLToast.show(text: "TA没有开启隐私设置\n无法使用飞鸽传书") return } else { selectedMemberIds.insert(id) diff --git a/QuickLocation/Service/GroupService.swift b/QuickLocation/Service/GroupService.swift index 3be7938e..6e634c01 100644 --- a/QuickLocation/Service/GroupService.swift +++ b/QuickLocation/Service/GroupService.swift @@ -235,4 +235,40 @@ struct GroupService { .do(onSuccess: notifyGroupInfoChange(operation: "dismiss", requestData: requestData)) .asObservable() } + + /// 提醒对方开启权限 + static func remindPrivilege( + content: String, + groupKey: String, + userId: String, + title: String = "请开启权限" + ) -> Observable { + let api = GroupAPI.privilegeRemind( + title: title, + content: content, + groupKey: groupKey, + userId: userId + ).multiTarget + return APIProvider.request(token: api) + .map(ResponseModel.self) + .asObservable() + } + + static func sendPrivilegeRemind(content: String, groupKey: String, userId: String) { + let key = groupKey.trimmingCharacters(in: .whitespacesAndNewlines) + let uid = userId.trimmingCharacters(in: .whitespacesAndNewlines) + guard !key.isEmpty, !uid.isEmpty else { return } + DLToast.showLoading() + remindPrivilege(content: content, groupKey: key, userId: uid) + .subscribe(onNext: { response in + guard response.code == "0" else { + DLToast.showError(text: response.message ?? "提醒失败") + return + } + DLToast.show(text: "已提醒") + }, onError: { error in + DLToast.show(text: error.localizedDescription) + }) + .disposed(by: disposeBag) + } } diff --git a/QuickLocation/UIKit/Alert/DLToast.swift b/QuickLocation/UIKit/Alert/DLToast.swift index fad1b5be..ca865c07 100644 --- a/QuickLocation/UIKit/Alert/DLToast.swift +++ b/QuickLocation/UIKit/Alert/DLToast.swift @@ -331,8 +331,6 @@ public class DLToast { if let toastView = superView.findSubView(with: DLToastConfig.tag) as? DLToastView { toastView.removeFromSuperview() } - let config = config - config.containerRadius = icon == nil ? 4 : config.containerRadius let toastView = DLToastView(iconImage: icon, text: text, config: config, style: style) toastView.tag = DLToastConfig.tag toastView.completion = completion