|
|
@ -2972,7 +2972,6 @@
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "AdHoc $(inherited)";
|
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ enum GroupAPI {
|
||||||
/// - opType: create、dismiss、setdefault
|
/// - opType: create、dismiss、setdefault
|
||||||
/// - requestData:[String: Any] 根据操作传入对应的值
|
/// - requestData:[String: Any] 根据操作传入对应的值
|
||||||
case operate(opType: String, requestData: [String: Any])
|
case operate(opType: String, requestData: [String: Any])
|
||||||
|
/// 提醒对方开启权限
|
||||||
|
case privilegeRemind(title: String, content: String, groupKey: String, userId: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension GroupAPI: MultiTargetProtocol {
|
extension GroupAPI: MultiTargetProtocol {
|
||||||
|
|
@ -36,6 +38,8 @@ extension GroupAPI: MultiTargetProtocol {
|
||||||
return "mapi/group/users"
|
return "mapi/group/users"
|
||||||
case .operate:
|
case .operate:
|
||||||
return "mapi/group/operate"
|
return "mapi/group/operate"
|
||||||
|
case .privilegeRemind:
|
||||||
|
return "mapi/group/privilege/remind"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,6 +68,14 @@ extension GroupAPI: MultiTargetProtocol {
|
||||||
params["op_type"] = opType
|
params["op_type"] = opType
|
||||||
params.merge(requestData) { _, new in new }
|
params.merge(requestData) { _, new in new }
|
||||||
return .requestParameters(parameters: params, encoding: JSONEncoding())
|
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,7 @@ extension UserAPI: MultiTargetProtocol {
|
||||||
switch self {
|
switch self {
|
||||||
case .userInfo, .userStatus, .phoneUsageToday, .phoneUsagePeriod, .phoneUsageReport, .phoneUsageTrips, .queryPhoneUsageIcon, .phoneLockApps, .phoneLocked, .phoneUnlockRequests, .phoneUnlockAllow, .signInInfo, .notice, .followList, .relations:
|
case .userInfo, .userStatus, .phoneUsageToday, .phoneUsagePeriod, .phoneUsageReport, .phoneUsageTrips, .queryPhoneUsageIcon, .phoneLockApps, .phoneLocked, .phoneUnlockRequests, .phoneUnlockAllow, .signInInfo, .notice, .followList, .relations:
|
||||||
return .get
|
return .get
|
||||||
case .changePhone, .setGender, .changeAvater:
|
case .changePhone, .changeAvater:
|
||||||
return .put
|
return .put
|
||||||
case .deleteAccount:
|
case .deleteAccount:
|
||||||
return .delete
|
return .delete
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/Common/image_broken.imageset/image_broken@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
QuickLocation/Assets.xcassets/Common/image_broken.imageset/image_broken@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 3.7 KiB |
22
QuickLocation/Assets.xcassets/Home/report_screen_tip_arrow.imageset/Contents.json
vendored
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/Home/report_screen_tip_arrow.imageset/report_screen_tip_arrow@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 246 B |
BIN
QuickLocation/Assets.xcassets/Home/report_screen_tip_arrow.imageset/report_screen_tip_arrow@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 390 B |
22
QuickLocation/Assets.xcassets/Home/report_usage_tip_arrow.imageset/Contents.json
vendored
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/Home/report_usage_tip_arrow.imageset/report_usage_tip_arrow@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 250 B |
BIN
QuickLocation/Assets.xcassets/Home/report_usage_tip_arrow.imageset/report_usage_tip_arrow@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 399 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 444 B |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 822 B |
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 438 B |
|
After Width: | Height: | Size: 960 B |
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"provides-namespace" : true
|
||||||
|
}
|
||||||
|
}
|
||||||
8
QuickLocation/Assets.xcassets/PrivilegeDenied/app_usage.imageset/Contents.json
vendored
Normal file
|
|
@ -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 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/PrivilegeDenied/app_usage.imageset/app_usage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
QuickLocation/Assets.xcassets/PrivilegeDenied/app_usage.imageset/app_usage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 56 KiB |
8
QuickLocation/Assets.xcassets/PrivilegeDenied/lock_apps.imageset/Contents.json
vendored
Normal file
|
|
@ -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 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/PrivilegeDenied/lock_apps.imageset/lock_apps@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
QuickLocation/Assets.xcassets/PrivilegeDenied/lock_apps.imageset/lock_apps@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 45 KiB |
8
QuickLocation/Assets.xcassets/PrivilegeDenied/lock_style.imageset/Contents.json
vendored
Normal file
|
|
@ -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 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/PrivilegeDenied/lock_style.imageset/lock_style@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
QuickLocation/Assets.xcassets/PrivilegeDenied/lock_style.imageset/lock_style@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 41 KiB |
8
QuickLocation/Assets.xcassets/PrivilegeDenied/phone_report.imageset/Contents.json
vendored
Normal file
|
|
@ -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 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/PrivilegeDenied/phone_report.imageset/phone_report@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
QuickLocation/Assets.xcassets/PrivilegeDenied/phone_report.imageset/phone_report@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 38 KiB |
8
QuickLocation/Assets.xcassets/PrivilegeDenied/report_screen.imageset/Contents.json
vendored
Normal file
|
|
@ -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 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/PrivilegeDenied/report_screen.imageset/report_screen@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
QuickLocation/Assets.xcassets/PrivilegeDenied/report_screen.imageset/report_screen@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 43 KiB |
8
QuickLocation/Assets.xcassets/PrivilegeDenied/report_status.imageset/Contents.json
vendored
Normal file
|
|
@ -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 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/PrivilegeDenied/report_status.imageset/report_status@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
QuickLocation/Assets.xcassets/PrivilegeDenied/report_status.imageset/report_status@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 51 KiB |
8
QuickLocation/Assets.xcassets/PrivilegeDenied/report_usage.imageset/Contents.json
vendored
Normal file
|
|
@ -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 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/PrivilegeDenied/report_usage.imageset/report_usage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
QuickLocation/Assets.xcassets/PrivilegeDenied/report_usage.imageset/report_usage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 50 KiB |
8
QuickLocation/Assets.xcassets/PrivilegeDenied/usage_duration.imageset/Contents.json
vendored
Normal file
|
|
@ -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 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/PrivilegeDenied/usage_duration.imageset/usage_duration@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
QuickLocation/Assets.xcassets/PrivilegeDenied/usage_duration.imageset/usage_duration@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 71 KiB |
8
QuickLocation/Assets.xcassets/PrivilegeDenied/usage_frequent.imageset/Contents.json
vendored
Normal file
|
|
@ -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 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/PrivilegeDenied/usage_frequent.imageset/usage_frequent@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
QuickLocation/Assets.xcassets/PrivilegeDenied/usage_frequent.imageset/usage_frequent@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 58 KiB |
|
|
@ -9,63 +9,63 @@ final class PrivilegeDeniedOverlay: UIView {
|
||||||
|
|
||||||
var onAction: (() -> Void)?
|
var onAction: (() -> Void)?
|
||||||
|
|
||||||
private let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
|
private let imageView = UIImageView()
|
||||||
private let dimView = UIView()
|
|
||||||
private let titleLab = UILabel()
|
private let titleLab = UILabel()
|
||||||
private let messageLab = UILabel()
|
private let messageLab = UILabel()
|
||||||
private let actionBtn = UIButton(type: .custom)
|
private let actionBtn = UIButton(type: .custom)
|
||||||
|
private let contentStack = UIStackView()
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
isHidden = true
|
isHidden = true
|
||||||
clipsToBounds = true
|
clipsToBounds = true
|
||||||
|
|
||||||
blurView.isUserInteractionEnabled = false
|
imageView.contentMode = .scaleAspectFill
|
||||||
addSubview(blurView)
|
imageView.clipsToBounds = true
|
||||||
blurView.layoutChain.edges()
|
imageView.isUserInteractionEnabled = false
|
||||||
|
addSubview(imageView)
|
||||||
dimView.backgroundColor = UIColor.white.withAlphaComponent(0.45)
|
imageView.layoutChain.edges()
|
||||||
dimView.isUserInteractionEnabled = false
|
|
||||||
addSubview(dimView)
|
|
||||||
dimView.layoutChain.edges()
|
|
||||||
|
|
||||||
titleLab.font = .systemFont(ofSize: 16, weight: .semibold)
|
titleLab.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||||
titleLab.textColor = UIColor(hexStr: "#293445")
|
titleLab.textColor = UIColor(hexStr: "#293445")
|
||||||
titleLab.textAlignment = .center
|
titleLab.textAlignment = .center
|
||||||
addSubview(titleLab)
|
|
||||||
|
|
||||||
messageLab.font = .systemFont(ofSize: 16, weight: .semibold)
|
messageLab.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||||
messageLab.textColor = UIColor(hexStr: "#293445")
|
messageLab.textColor = UIColor(hexStr: "#293445")
|
||||||
messageLab.textAlignment = .center
|
messageLab.textAlignment = .center
|
||||||
addSubview(messageLab)
|
|
||||||
|
|
||||||
actionBtn.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
|
actionBtn.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||||
actionBtn.setTitleColor(.white, for: .normal)
|
actionBtn.setTitleColor(.white, for: .normal)
|
||||||
actionBtn.backgroundColor = UIColor(hexStr: "#16B3FF")
|
actionBtn.backgroundColor = UIColor(hexStr: "#16B3FF")
|
||||||
actionBtn.cornerRadius = 10
|
actionBtn.cornerRadius = 10
|
||||||
actionBtn.addTarget(self, action: #selector(tapAction), for: .touchUpInside)
|
actionBtn.addTarget(self, action: #selector(tapAction), for: .touchUpInside)
|
||||||
addSubview(actionBtn)
|
actionBtn.layoutChain.width(160).height(44)
|
||||||
|
|
||||||
titleLab.layoutChain.centerX()
|
contentStack.axis = .vertical
|
||||||
titleLab.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -28).isActive = true
|
contentStack.alignment = .center
|
||||||
messageLab.layoutChain
|
contentStack.spacing = 6
|
||||||
.topToBottomOfView(titleLab, offset: 6)
|
contentStack.addArrangedSubview(titleLab)
|
||||||
.centerX()
|
contentStack.addArrangedSubview(messageLab)
|
||||||
actionBtn.layoutChain
|
contentStack.setCustomSpacing(16, after: messageLab)
|
||||||
.topToBottomOfView(messageLab, offset: 16)
|
contentStack.addArrangedSubview(actionBtn)
|
||||||
.centerX()
|
addSubview(contentStack)
|
||||||
.width(160)
|
contentStack.layoutChain.centerX().centerY()
|
||||||
.height(44)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
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
|
titleLab.text = title
|
||||||
messageLab.text = message
|
messageLab.text = message
|
||||||
actionBtn.setTitle(actionTitle, for: .normal)
|
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) {
|
func setVisible(_ visible: Bool) {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ extension UIDevice {
|
||||||
deviceModelMap[machineIdentifier] ?? "iPhone"
|
deviceModelMap[machineIdentifier] ?? "iPhone"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 与系统状态栏一致的电量百分比(四舍五入);不可用时返回 `nil`
|
/// 本机电量百分比。公开 API 只有 `UIDevice.batteryLevel`,精度约 5%,无法与状态栏 1% 完全对齐。
|
||||||
static var batteryPercent: Int? {
|
static var batteryPercent: Int? {
|
||||||
let device = UIDevice.current
|
let device = UIDevice.current
|
||||||
if !device.isBatteryMonitoringEnabled {
|
if !device.isBatteryMonitoringEnabled {
|
||||||
|
|
@ -29,7 +29,7 @@ extension UIDevice {
|
||||||
}
|
}
|
||||||
let level = device.batteryLevel
|
let level = device.batteryLevel
|
||||||
guard level >= 0 else { return nil }
|
guard level >= 0 else { return nil }
|
||||||
return Int((level * 100).rounded())
|
return Int((Double(level) * 100).rounded())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 是否正在充电 / 已充满接电
|
/// 是否正在充电 / 已充满接电
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ struct UserConfigModel: Mappable {
|
||||||
temp <- map["temp"]
|
temp <- map["temp"]
|
||||||
phone <- map["phone"]
|
phone <- map["phone"]
|
||||||
name <- map["name"]
|
name <- map["name"]
|
||||||
uid <- map["user_id"]
|
uid <- (map["user_id"], kFlexString)
|
||||||
avater <- map["avater"]
|
avater <- map["avater"]
|
||||||
sex <- map["sex"]
|
sex <- map["sex"]
|
||||||
head_pic <- (map["head_pic"], kFlexString)
|
head_pic <- (map["head_pic"], kFlexString)
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ enum MqttType: String, Codable {
|
||||||
case kick = "kick" // 踢人
|
case kick = "kick" // 踢人
|
||||||
case signIn = "signIn" // 签到
|
case signIn = "signIn" // 签到
|
||||||
case sos = "sos" // 求助
|
case sos = "sos" // 求助
|
||||||
|
case sosQuit = "sosquit" // 取消求助
|
||||||
|
case bubbleQuit = "bubblequit" // 退出气泡
|
||||||
case needTrack = "needtrack" // 追踪上报
|
case needTrack = "needtrack" // 追踪上报
|
||||||
case emote = "emote" // 接收表情
|
case emote = "emote" // 接收表情
|
||||||
case phone = "phone" // 手机信息上报
|
case phone = "phone" // 手机信息上报
|
||||||
|
|
@ -1022,9 +1024,7 @@ final class MQTTService: NSObject {
|
||||||
func reportLocation(lat: Double, lon: Double, addr: String,
|
func reportLocation(lat: Double, lon: Double, addr: String,
|
||||||
speed: CLLocationSpeed, bearing: CLLocationDirection,
|
speed: CLLocationSpeed, bearing: CLLocationDirection,
|
||||||
altitude: CLLocationDistance, accuracy: CLLocationAccuracy) {
|
altitude: CLLocationDistance, accuracy: CLLocationAccuracy) {
|
||||||
let battery = UIDevice.current.batteryLevel > 0
|
let battery = UIDevice.batteryPercent.map(String.init) ?? ""
|
||||||
? Int(UIDevice.current.batteryLevel * 100)
|
|
||||||
: 0
|
|
||||||
|
|
||||||
let point = Points(
|
let point = Points(
|
||||||
lat: lat, lon: lon, addr: addr,
|
lat: lat, lon: lon, addr: addr,
|
||||||
|
|
@ -1032,7 +1032,7 @@ final class MQTTService: NSObject {
|
||||||
speed: speed, bearing: bearing,
|
speed: speed, bearing: bearing,
|
||||||
altitude: altitude, accuracy: accuracy
|
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),
|
guard let jsonData = try? JSONEncoder().encode(location),
|
||||||
let dataDict = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any]
|
let dataDict = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any]
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import AVFoundation
|
||||||
import CoreTelephony
|
import CoreTelephony
|
||||||
import Foundation
|
import Foundation
|
||||||
import Network
|
import Network
|
||||||
|
import NetworkExtension
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
struct PhoneStatusSnapshot: Equatable {
|
struct PhoneStatusSnapshot: Equatable {
|
||||||
|
|
@ -67,7 +68,7 @@ final class PhoneStatusCollector: NSObject {
|
||||||
)
|
)
|
||||||
center.addObserver(
|
center.addObserver(
|
||||||
self,
|
self,
|
||||||
selector: #selector(handleChange),
|
selector: #selector(handleApplicationDidBecomeActive),
|
||||||
name: UIApplication.didBecomeActiveNotification,
|
name: UIApplication.didBecomeActiveNotification,
|
||||||
object: nil
|
object: nil
|
||||||
)
|
)
|
||||||
|
|
@ -119,6 +120,11 @@ final class PhoneStatusCollector: NSObject {
|
||||||
emitChange()
|
emitChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func handleApplicationDidBecomeActive() {
|
||||||
|
networkMonitor.refreshWiFiName()
|
||||||
|
emitChange()
|
||||||
|
}
|
||||||
|
|
||||||
private func emitChange() {
|
private func emitChange() {
|
||||||
let work = {
|
let work = {
|
||||||
self.pendingNotify?.cancel()
|
self.pendingNotify?.cancel()
|
||||||
|
|
@ -163,41 +169,68 @@ final class PhoneStatusCollector: NSObject {
|
||||||
|
|
||||||
private final class PhoneNetworkStatusMonitor {
|
private final class PhoneNetworkStatusMonitor {
|
||||||
|
|
||||||
private let monitor = NWPathMonitor()
|
private let monitor = Network.NWPathMonitor()
|
||||||
private let queue = DispatchQueue(label: "com.quicklocation.phone.network")
|
private let queue = DispatchQueue(label: "com.quicklocation.phone.network")
|
||||||
private let lock = NSLock()
|
private let lock = NSLock()
|
||||||
private let telephonyInfo = CTTelephonyNetworkInfo()
|
private let telephonyInfo = CTTelephonyNetworkInfo()
|
||||||
private var path: NWPath?
|
private var path: Network.NWPath?
|
||||||
|
private var wifiName: String?
|
||||||
private var onChange: (() -> Void)?
|
private var onChange: (() -> Void)?
|
||||||
|
|
||||||
func start(onChange: @escaping () -> Void) {
|
func start(onChange: @escaping () -> Void) {
|
||||||
self.onChange = onChange
|
self.onChange = onChange
|
||||||
monitor.pathUpdateHandler = { [weak self] path in
|
monitor.pathUpdateHandler = { [weak self] path in
|
||||||
self?.lock.lock()
|
guard let self else { return }
|
||||||
self?.path = path
|
self.lock.lock()
|
||||||
self?.lock.unlock()
|
self.path = path
|
||||||
|
self.lock.unlock()
|
||||||
|
if path.status == .satisfied,
|
||||||
|
path.usesInterfaceType(Network.NWInterface.InterfaceType.wifi) {
|
||||||
|
self.refreshWiFiName()
|
||||||
|
} else {
|
||||||
|
self.updateWiFiName(nil)
|
||||||
|
}
|
||||||
onChange()
|
onChange()
|
||||||
}
|
}
|
||||||
monitor.start(queue: queue)
|
monitor.start(queue: queue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func refreshWiFiName() {
|
||||||
|
NEHotspotNetwork.fetchCurrent { [weak self] network in
|
||||||
|
self?.updateWiFiName(network?.ssid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var statusText: String {
|
var statusText: String {
|
||||||
lock.lock()
|
lock.lock()
|
||||||
let currentPath = path
|
let currentPath = path
|
||||||
|
let currentWiFiName = wifiName
|
||||||
lock.unlock()
|
lock.unlock()
|
||||||
|
|
||||||
guard let currentPath, currentPath.status == .satisfied else {
|
guard let currentPath, currentPath.status == .satisfied else {
|
||||||
return "无网络"
|
return "无网络"
|
||||||
}
|
}
|
||||||
if currentPath.usesInterfaceType(.wifi) {
|
if currentPath.usesInterfaceType(Network.NWInterface.InterfaceType.wifi) {
|
||||||
return "Wi-Fi"
|
return currentWiFiName ?? "Wi-Fi"
|
||||||
}
|
}
|
||||||
guard currentPath.usesInterfaceType(.cellular) else {
|
guard currentPath.usesInterfaceType(Network.NWInterface.InterfaceType.cellular) else {
|
||||||
return "无网络"
|
return "无网络"
|
||||||
}
|
}
|
||||||
return cellularGeneration
|
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 {
|
private var cellularGeneration: String {
|
||||||
let technologies = telephonyInfo.serviceCurrentRadioAccessTechnology
|
let technologies = telephonyInfo.serviceCurrentRadioAccessTechnology
|
||||||
.map { Array($0.values) } ?? []
|
.map { Array($0.values) } ?? []
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ struct GroupUsersResponse: BaseModelProtocol {
|
||||||
var message: String?
|
var message: String?
|
||||||
var model: GroupInfoModel?
|
var model: GroupInfoModel?
|
||||||
var list: [GroupMemberModel] = []
|
var list: [GroupMemberModel] = []
|
||||||
|
var reviewCount: Int = 0
|
||||||
|
|
||||||
init?(map: Map) {}
|
init?(map: Map) {}
|
||||||
|
|
||||||
|
|
@ -100,6 +101,10 @@ struct GroupUsersResponse: BaseModelProtocol {
|
||||||
message <- map["message"]
|
message <- map["message"]
|
||||||
model <- map["data.group"]
|
model <- map["data.group"]
|
||||||
list <- map["data.mates"]
|
list <- map["data.mates"]
|
||||||
|
reviewCount <- (map["data.reviewCount"], kStrTransformInt)
|
||||||
|
if reviewCount == 0 {
|
||||||
|
reviewCount <- (map["data.review_count"], kStrTransformInt)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isValid(for groupKey: String) -> Bool {
|
func isValid(for groupKey: String) -> Bool {
|
||||||
|
|
@ -310,11 +315,11 @@ extension UIImageView {
|
||||||
kf.cancelDownloadTask()
|
kf.cancelDownloadTask()
|
||||||
image = fallback
|
image = fallback
|
||||||
guard let imageURL = URL(string: url), !url.isEmpty else { return }
|
guard let imageURL = URL(string: url), !url.isEmpty else { return }
|
||||||
kf.setImage(with: imageURL, placeholder: fallback) { [weak self] result in
|
kf.setImage(
|
||||||
if case .failure = result {
|
with: imageURL,
|
||||||
self?.image = fallback
|
placeholder: fallback,
|
||||||
}
|
options: [.onFailureImage(kBrokenNetworkImage)]
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -478,7 +483,7 @@ struct GroupMemberModel: Mappable, Equatable {
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func mapping(map: Map) {
|
mutating func mapping(map: Map) {
|
||||||
user_id <- map["user_id"]
|
user_id <- (map["user_id"], kFlexString)
|
||||||
head_pic <- (map["head_pic"], kFlexString)
|
head_pic <- (map["head_pic"], kFlexString)
|
||||||
avater <- (map["avater"], kFlexString)
|
avater <- (map["avater"], kFlexString)
|
||||||
if avater.isEmpty {
|
if avater.isEmpty {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,16 @@ import UIKit
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
import CoreMedia
|
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 {
|
public protocol ImageSource {
|
||||||
/// 图片资源URL
|
/// 图片资源URL
|
||||||
var imageURL: URL? { get }
|
var imageURL: URL? { get }
|
||||||
|
|
@ -60,15 +70,18 @@ extension DLWrapper where Base: UIImageView {
|
||||||
base.image = image
|
base.image = image
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 都不存在,则置为nil
|
base.image = placeholder ?? .brokenNetworkImage
|
||||||
base.image = placeholder
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是视频, 获取视频第一帧
|
// 如果是视频, 获取视频第一帧
|
||||||
if imageURL.imageStr?.contains(".mp4") == true {
|
if imageURL.imageStr?.contains(".mp4") == true {
|
||||||
let source = AVAssetImageDataProvider(assetURL: imageURL, time: CMTimeMakeWithSeconds(0.0, preferredTimescale: 1))
|
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 {
|
switch result {
|
||||||
case let .success(imageinfo):
|
case let .success(imageinfo):
|
||||||
completionHandler?(imageinfo.image, nil)
|
completionHandler?(imageinfo.image, nil)
|
||||||
|
|
@ -77,8 +90,11 @@ extension DLWrapper where Base: UIImageView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 使用kf加载图片
|
base.kf.setImage(
|
||||||
base.kf.setImage(with: imageURL, placeholder: placeholder) { result in
|
with: imageURL,
|
||||||
|
placeholder: placeholder,
|
||||||
|
options: kNetworkImageFailureOptions
|
||||||
|
) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case let .success(imageinfo):
|
case let .success(imageinfo):
|
||||||
completionHandler?(imageinfo.image, nil)
|
completionHandler?(imageinfo.image, nil)
|
||||||
|
|
@ -116,18 +132,23 @@ extension DLWrapper where Base: UIButton {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 都不存在,则置为nil
|
// 都不存在,则置为nil
|
||||||
base.setImage(placeholder, for: state)
|
base.setImage(placeholder ?? .brokenNetworkImage, for: state)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 使用kf加载图片
|
base.kf.setImage(
|
||||||
base.kf.setImage(with: imageURL, for: state, placeholder: placeholder, completionHandler: { result in
|
with: imageURL,
|
||||||
|
for: state,
|
||||||
|
placeholder: placeholder,
|
||||||
|
options: kNetworkImageFailureOptions,
|
||||||
|
completionHandler: { result in
|
||||||
switch result {
|
switch result {
|
||||||
case let .success(imageinfo):
|
case let .success(imageinfo):
|
||||||
completionHandler?(imageinfo.image, nil)
|
completionHandler?(imageinfo.image, nil)
|
||||||
case let .failure(error):
|
case let .failure(error):
|
||||||
completionHandler?(nil, error)
|
completionHandler?(nil, error)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 设置按钮背景图片
|
/// 设置按钮背景图片
|
||||||
|
|
@ -153,18 +174,23 @@ extension DLWrapper where Base: UIButton {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 都不存在,则置为nil
|
// 都不存在,则置为nil
|
||||||
base.setBackgroundImage(placeholder, for: state)
|
base.setBackgroundImage(placeholder ?? .brokenNetworkImage, for: state)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 使用kf加载图片
|
base.kf.setBackgroundImage(
|
||||||
base.kf.setBackgroundImage(with: imageURL, for: state, placeholder: placeholder, completionHandler: { result in
|
with: imageURL,
|
||||||
|
for: state,
|
||||||
|
placeholder: placeholder,
|
||||||
|
options: kNetworkImageFailureOptions,
|
||||||
|
completionHandler: { result in
|
||||||
switch result {
|
switch result {
|
||||||
case let .success(imageinfo):
|
case let .success(imageinfo):
|
||||||
completionHandler?(imageinfo.image, nil)
|
completionHandler?(imageinfo.image, nil)
|
||||||
case let .failure(error):
|
case let .failure(error):
|
||||||
completionHandler?(nil, error)
|
completionHandler?(nil, error)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -220,7 +246,7 @@ enum HeadPic {
|
||||||
case let .success(value):
|
case let .success(value):
|
||||||
image = value.image
|
image = value.image
|
||||||
case .failure:
|
case .failure:
|
||||||
image = UIImage()
|
image = .brokenNetworkImage
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
completion(image)
|
completion(image)
|
||||||
|
|
@ -239,8 +265,7 @@ extension UIImageView {
|
||||||
}
|
}
|
||||||
if let url = HeadPic.remoteURL(for: trimmed) {
|
if let url = HeadPic.remoteURL(for: trimmed) {
|
||||||
kf.cancelDownloadTask()
|
kf.cancelDownloadTask()
|
||||||
image = nil
|
kf.setImage(with: url, placeholder: nil, options: kNetworkImageFailureOptions)
|
||||||
kf.setImage(with: url, placeholder: nil)
|
|
||||||
} else {
|
} else {
|
||||||
kf.cancelDownloadTask()
|
kf.cancelDownloadTask()
|
||||||
image = HeadPic.localImage(trimmed)
|
image = HeadPic.localImage(trimmed)
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ final class FeatureIntroVC: BaseViewController {
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(animated)
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
rootView.reloadFeatureItems()
|
||||||
guard let config = AppContextManager.shared.systemConfig else { return }
|
guard let config = AppContextManager.shared.systemConfig else { return }
|
||||||
bannerList = config.bannerList
|
bannerList = config.bannerList
|
||||||
rootView.cycleScrollView.isHidden = config.bannerList.filter { $0.enable == true }.count == 0
|
rootView.cycleScrollView.isHidden = config.bannerList.filter { $0.enable == true }.count == 0
|
||||||
|
|
@ -56,6 +57,7 @@ final class FeatureIntroVC: BaseViewController {
|
||||||
case .pigeonMessage:
|
case .pigeonMessage:
|
||||||
AppRouter.push(Route.pigeonMessage)
|
AppRouter.push(Route.pigeonMessage)
|
||||||
case .searchLocation:
|
case .searchLocation:
|
||||||
|
guard AppContextManager.shared.systemConfig?.isIntercept != true else { return }
|
||||||
AppRouter.push(Route.searchLocation)
|
AppRouter.push(Route.searchLocation)
|
||||||
case .sos:
|
case .sos:
|
||||||
AppRouter.push(Route.sos)
|
AppRouter.push(Route.sos)
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ final class FeatureIntroView: UIView {
|
||||||
private let contentView = UIView()
|
private let contentView = UIView()
|
||||||
private let gridStack = UIStackView()
|
private let gridStack = UIStackView()
|
||||||
|
|
||||||
private let items: [FeatureIntroItem] = [
|
private static let allItems: [FeatureIntroItem] = [
|
||||||
FeatureIntroItem(
|
FeatureIntroItem(
|
||||||
imageName: "Explore/sign_in_card",
|
imageName: "Explore/sign_in_card",
|
||||||
accessibilityTitle: "还在吗",
|
accessibilityTitle: "还在吗",
|
||||||
|
|
@ -76,6 +76,8 @@ final class FeatureIntroView: UIView {
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
private var displayedItems: [FeatureIntroItem] = []
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
setupUI()
|
setupUI()
|
||||||
|
|
@ -118,25 +120,7 @@ final class FeatureIntroView: UIView {
|
||||||
gridStack.spacing = 13
|
gridStack.spacing = 13
|
||||||
contentView.addSubview(gridStack)
|
contentView.addSubview(gridStack)
|
||||||
gridStack.translatesAutoresizingMaskIntoConstraints = false
|
gridStack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
reloadFeatureItems()
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
headerBackgroundView.layoutChain
|
headerBackgroundView.layoutChain
|
||||||
.edges(excludingEdge: .bottom)
|
.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..<columns {
|
||||||
|
if index < displayedItems.count {
|
||||||
|
let item = displayedItems[index]
|
||||||
|
let card = FeatureIntroCardView(item: item)
|
||||||
|
card.tag = index
|
||||||
|
card.addTarget(self, action: #selector(cardTapped(_:)), for: .touchUpInside)
|
||||||
|
rowStack.addArrangedSubview(card)
|
||||||
|
card.heightAnchor.constraint(equalTo: card.widthAnchor, multiplier: item.aspectRatio).isActive = true
|
||||||
|
} else {
|
||||||
|
rowStack.addArrangedSubview(UIView())
|
||||||
|
}
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
gridStack.addArrangedSubview(rowStack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func cardTapped(_ sender: FeatureIntroCardView) {
|
@objc private func cardTapped(_ sender: FeatureIntroCardView) {
|
||||||
guard items.indices.contains(sender.tag) else { return }
|
guard displayedItems.indices.contains(sender.tag) else { return }
|
||||||
onTapItem?(items[sender.tag].action)
|
onTapItem?(displayedItems[sender.tag].action)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 轮播图
|
// MARK: - 轮播图
|
||||||
|
|
|
||||||
|
|
@ -495,9 +495,12 @@ final class GroupChatVC: BaseViewController {
|
||||||
}
|
}
|
||||||
self.viewModel.setPendingQuote(oim)
|
self.viewModel.setPendingQuote(oim)
|
||||||
}
|
}
|
||||||
|
menu.onReport = {
|
||||||
|
ReceiveMessageReportPopView.show()
|
||||||
|
}
|
||||||
menu.onDismiss = { [weak self] in self?.bubbleMenu = nil }
|
menu.onDismiss = { [weak self] in self?.bubbleMenu = nil }
|
||||||
let anchor = menuAnchorView(in: cell)
|
let anchor = menuAnchorView(in: cell)
|
||||||
menu.show(in: rootView, anchor: anchor, canMention: !msg.isSelf)
|
menu.show(in: rootView, anchor: anchor, canMention: !msg.isSelf, canReport: !msg.isSelf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Mention
|
// MARK: - Mention
|
||||||
|
|
|
||||||
|
|
@ -974,6 +974,7 @@ private final class ChatPaddingLabel: UILabel {
|
||||||
final class ChatBubbleActionMenu: UIView {
|
final class ChatBubbleActionMenu: UIView {
|
||||||
var onMention: (() -> Void)?
|
var onMention: (() -> Void)?
|
||||||
var onReply: (() -> Void)?
|
var onReply: (() -> Void)?
|
||||||
|
var onReport: (() -> Void)?
|
||||||
var onDismiss: (() -> Void)?
|
var onDismiss: (() -> Void)?
|
||||||
|
|
||||||
private let panel = UIView()
|
private let panel = UIView()
|
||||||
|
|
@ -1025,7 +1026,7 @@ final class ChatBubbleActionMenu: UIView {
|
||||||
|
|
||||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
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()
|
removeFromSuperview()
|
||||||
host.addSubview(self)
|
host.addSubview(self)
|
||||||
frame = host.bounds
|
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: "TA", imageName: "IM/at", action: #selector(handleMention)))
|
||||||
}
|
}
|
||||||
stack.addArrangedSubview(makeButton(title: "回复", imageName: "IM/reply", action: #selector(handleReply)))
|
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 panelW = itemWidth * CGFloat(max(stack.arrangedSubviews.count, 1))
|
||||||
let panelH = panelHeight
|
let panelH = panelHeight
|
||||||
|
|
@ -1111,6 +1115,11 @@ final class ChatBubbleActionMenu: UIView {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func handleReport() {
|
||||||
|
onReport?()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func handleDismiss() { dismiss() }
|
@objc private func handleDismiss() { dismiss() }
|
||||||
|
|
||||||
func dismiss() {
|
func dismiss() {
|
||||||
|
|
@ -2086,14 +2095,25 @@ class ChatImageMsgCell: UITableViewCell {
|
||||||
|
|
||||||
private func loadPhoto(url: String, messageId: String) {
|
private func loadPhoto(url: String, messageId: String) {
|
||||||
photoView.kf.cancelDownloadTask()
|
photoView.kf.cancelDownloadTask()
|
||||||
guard !url.isEmpty else { photoView.image = nil; return }
|
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) {
|
if let localImage = UIImage(contentsOfFile: url) {
|
||||||
photoView.image = localImage
|
applyPhoto(localImage, broken: false)
|
||||||
|
} else {
|
||||||
|
applyPhoto(nil, broken: true)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
photoView.dl.setImage(with: url) { [weak self] image, _ in
|
photoView.dl.setImage(with: url) { [weak self] image, _ in
|
||||||
guard let self, self.configuredId == messageId, let image else { return }
|
guard let self, self.configuredId == messageId else { return }
|
||||||
self.photoView.image = image
|
applyPhoto(image, broken: image == nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ final class GroupItineraryView: UIView {
|
||||||
addSubview(createBtn)
|
addSubview(createBtn)
|
||||||
|
|
||||||
headerRow.layoutChain
|
headerRow.layoutChain
|
||||||
.top(8)
|
.top(20)
|
||||||
.edgesHorzontal(16)
|
.edgesHorzontal(16)
|
||||||
.height(28)
|
.height(28)
|
||||||
|
|
||||||
|
|
@ -323,12 +323,13 @@ final class GroupItineraryCell: UITableViewCell {
|
||||||
tagScrollView.translatesAutoresizingMaskIntoConstraints = false
|
tagScrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
tagStackView.translatesAutoresizingMaskIntoConstraints = false
|
tagStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
startMarkerLab.layoutChain
|
||||||
startMarkerLab.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 14),
|
.topToBottomOfView(avatarImg, offset: 10)
|
||||||
startMarkerLab.topAnchor.constraint(equalTo: avatarImg.bottomAnchor, constant: 10),
|
.leftToView(nameLab)
|
||||||
startMarkerLab.widthAnchor.constraint(equalToConstant: 20),
|
.width(20)
|
||||||
startMarkerLab.heightAnchor.constraint(equalToConstant: 20),
|
.heightToWidth(1)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
startLocationLab.leadingAnchor.constraint(equalTo: startMarkerLab.trailingAnchor, constant: 7),
|
startLocationLab.leadingAnchor.constraint(equalTo: startMarkerLab.trailingAnchor, constant: 7),
|
||||||
startLocationLab.centerYAnchor.constraint(equalTo: startMarkerLab.centerYAnchor),
|
startLocationLab.centerYAnchor.constraint(equalTo: startMarkerLab.centerYAnchor),
|
||||||
startLocationLab.widthAnchor.constraint(lessThanOrEqualToConstant: 82),
|
startLocationLab.widthAnchor.constraint(lessThanOrEqualToConstant: 82),
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,8 @@ final class GroupMemberDirectoryCell: UITableViewCell {
|
||||||
view.backgroundColor = UIColor(hexStr: "#F1F2F4")
|
view.backgroundColor = UIColor(hexStr: "#F1F2F4")
|
||||||
view.contentMode = .scaleAspectFill
|
view.contentMode = .scaleAspectFill
|
||||||
view.layer.cornerRadius = 10
|
view.layer.cornerRadius = 10
|
||||||
|
view.layer.borderWidth = 2
|
||||||
|
view.layer.borderColor = UIColor.white.cgColor
|
||||||
view.clipsToBounds = true
|
view.clipsToBounds = true
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
@ -338,6 +340,8 @@ final class GroupMemberListCell: UICollectionViewCell {
|
||||||
view.backgroundColor = UIColor(hexStr: "#F1F2F4")
|
view.backgroundColor = UIColor(hexStr: "#F1F2F4")
|
||||||
view.contentMode = .scaleAspectFill
|
view.contentMode = .scaleAspectFill
|
||||||
view.layer.cornerRadius = 25
|
view.layer.cornerRadius = 25
|
||||||
|
view.layer.borderWidth = 2
|
||||||
|
view.layer.borderColor = UIColor.white.cgColor
|
||||||
view.clipsToBounds = true
|
view.clipsToBounds = true
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ class GroupSettingVC: BaseViewController {
|
||||||
|
|
||||||
private var viewModel: GroupSettingViewModel
|
private var viewModel: GroupSettingViewModel
|
||||||
|
|
||||||
|
private var hasAppeared = false
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
|
@ -31,6 +33,14 @@ class GroupSettingVC: BaseViewController {
|
||||||
requestGroupInfo()
|
requestGroupInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
if hasAppeared {
|
||||||
|
requestReviewBadge()
|
||||||
|
}
|
||||||
|
hasAppeared = true
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Actions
|
// MARK: - Actions
|
||||||
private func reactiveAction() {
|
private func reactiveAction() {
|
||||||
// 更换名称
|
// 更换名称
|
||||||
|
|
@ -108,6 +118,11 @@ class GroupSettingVC: BaseViewController {
|
||||||
self?.confirmClearHistory()
|
self?.confirmClearHistory()
|
||||||
}).disposed(by: disposeBag)
|
}).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
|
rootView.dismissGroupView.rx.tapGesture.subscribe(onNext: { _ in
|
||||||
VerificationPopView.show {
|
VerificationPopView.show {
|
||||||
|
|
@ -127,6 +142,18 @@ class GroupSettingVC: BaseViewController {
|
||||||
}).disposed(by: disposeBag)
|
}).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
|
// MARK: - API
|
||||||
private func requestGroupInfo() {
|
private func requestGroupInfo() {
|
||||||
DLToast.showLoading()
|
DLToast.showLoading()
|
||||||
|
|
@ -144,7 +171,9 @@ class GroupSettingVC: BaseViewController {
|
||||||
self.rootView.setupData(model,
|
self.rootView.setupData(model,
|
||||||
members: response.list,
|
members: response.list,
|
||||||
groupNickname: self.viewModel.displayGroupNickname)
|
groupNickname: self.viewModel.displayGroupNickname)
|
||||||
|
self.rootView.setAuditBadgeVisible(model.is_owner && response.reviewCount > 0)
|
||||||
self.requestGroupNickname()
|
self.requestGroupNickname()
|
||||||
|
self.requestReviewBadge()
|
||||||
}, onError: { error in
|
}, onError: { error in
|
||||||
DLToast.dismiss()
|
DLToast.dismiss()
|
||||||
DLToast.showError(text: error.localizedDescription)
|
DLToast.showError(text: error.localizedDescription)
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,12 @@ final class GroupSettingView: UIView {
|
||||||
switchBtn.isOn = model.review
|
switchBtn.isOn = model.review
|
||||||
auditSwitchView.isHidden = !model.is_owner
|
auditSwitchView.isHidden = !model.is_owner
|
||||||
auditMemberView.isHidden = !model.is_owner
|
auditMemberView.isHidden = !model.is_owner
|
||||||
|
if !model.is_owner {
|
||||||
|
auditMemberView.setShowsBadge(false)
|
||||||
|
}
|
||||||
dismissGroupView.isHidden = !model.is_owner
|
dismissGroupView.isHidden = !model.is_owner
|
||||||
leaveGroupView.isHidden = model.is_owner
|
leaveGroupView.isHidden = model.is_owner
|
||||||
|
reportGroupView.isHidden = model.is_owner
|
||||||
|
|
||||||
groupNameView.setShowsArrow(model.is_owner)
|
groupNameView.setShowsArrow(model.is_owner)
|
||||||
groupTypeView.setShowsArrow(false)
|
groupTypeView.setShowsArrow(false)
|
||||||
|
|
@ -47,6 +51,10 @@ final class GroupSettingView: UIView {
|
||||||
groupNicknameView.setValue(nickname)
|
groupNicknameView.setValue(nickname)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setAuditBadgeVisible(_ visible: Bool) {
|
||||||
|
auditMemberView.setShowsBadge(visible)
|
||||||
|
}
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
backgroundColor = UIColor(hexStr: "#F7F8FA")
|
backgroundColor = UIColor(hexStr: "#F7F8FA")
|
||||||
|
|
@ -212,6 +220,7 @@ final class GroupSettingView: UIView {
|
||||||
dangerStackView.translatesAutoresizingMaskIntoConstraints = false
|
dangerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
NSLayoutConstraint.activate(dangerStackView.edgesToSuperview())
|
NSLayoutConstraint.activate(dangerStackView.edgesToSuperview())
|
||||||
dangerStackView.addArrangedSubview(dismissGroupView)
|
dangerStackView.addArrangedSubview(dismissGroupView)
|
||||||
|
dangerStackView.addArrangedSubview(reportGroupView)
|
||||||
dangerStackView.addArrangedSubview(leaveGroupView)
|
dangerStackView.addArrangedSubview(leaveGroupView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -296,7 +305,7 @@ final class GroupSettingView: UIView {
|
||||||
private let memberSeparatorView = HorizontalDashLineView()
|
private let memberSeparatorView = HorizontalDashLineView()
|
||||||
private let memberActionsView = UIView()
|
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")
|
lazy var shareButton: UIButton = makeMemberActionButton(title: "分享", imageName: "GroupSetting/share")
|
||||||
|
|
||||||
private lazy var actionDividerView: UIView = {
|
private lazy var actionDividerView: UIView = {
|
||||||
|
|
@ -343,6 +352,7 @@ final class GroupSettingView: UIView {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let dismissGroupView = GroupSettingActionRow(title: "解散圈子", titleColor: UIColor(hexStr: "#FF4D53"), hidesDivider: true)
|
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)
|
let leaveGroupView = GroupSettingActionRow(title: "退出圈子", titleColor: UIColor(hexStr: "#FF4D53"), hidesDivider: true)
|
||||||
|
|
||||||
private func makeCardView() -> UIView {
|
private func makeCardView() -> UIView {
|
||||||
|
|
@ -558,6 +568,7 @@ final class GroupSettingInfoRow: UIView {
|
||||||
|
|
||||||
private let style: Style
|
private let style: Style
|
||||||
private let titleLab = UILabel()
|
private let titleLab = UILabel()
|
||||||
|
private let badgeView = UIView()
|
||||||
private let valueLab = UILabel()
|
private let valueLab = UILabel()
|
||||||
private let valueImageView = UIImageView()
|
private let valueImageView = UIImageView()
|
||||||
private let arrowImageView = UIImageView(image: UIImage(named: "GroupSetting/arrow_right"))
|
private let arrowImageView = UIImageView(image: UIImage(named: "GroupSetting/arrow_right"))
|
||||||
|
|
@ -596,6 +607,10 @@ final class GroupSettingInfoRow: UIView {
|
||||||
imageTrailingToRow?.isActive = !showsArrow
|
imageTrailingToRow?.isActive = !showsArrow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setShowsBadge(_ showsBadge: Bool) {
|
||||||
|
badgeView.isHidden = !showsBadge
|
||||||
|
}
|
||||||
|
|
||||||
private func setupUI() {
|
private func setupUI() {
|
||||||
backgroundColor = .clear
|
backgroundColor = .clear
|
||||||
titleLab.textColor = UIColor(hexStr: "#293445")
|
titleLab.textColor = UIColor(hexStr: "#293445")
|
||||||
|
|
@ -613,14 +628,21 @@ final class GroupSettingInfoRow: UIView {
|
||||||
arrowImageView.contentMode = .scaleAspectFit
|
arrowImageView.contentMode = .scaleAspectFit
|
||||||
dividerView.backgroundColor = UIColor(hexStr: "#EDEEF0")
|
dividerView.backgroundColor = UIColor(hexStr: "#EDEEF0")
|
||||||
switchButton.onTintColor = UIColor(hexStr: "#16B3FF")
|
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)
|
addSubview($0)
|
||||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||||
}
|
}
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
titleLab.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
|
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.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
|
||||||
arrowImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
arrowImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
arrowImageView.widthAnchor.constraint(equalToConstant: 16),
|
arrowImageView.widthAnchor.constraint(equalToConstant: 16),
|
||||||
|
|
@ -733,8 +755,8 @@ final class GroupSettingTagRow: UIView {
|
||||||
$0.removeFromSuperview()
|
$0.removeFromSuperview()
|
||||||
}
|
}
|
||||||
if tags.count > 2 {
|
if tags.count > 2 {
|
||||||
tagStack.addArrangedSubview(GroupSettingTagChip(text: "···", compact: true))
|
|
||||||
tags.suffix(2).forEach { tagStack.addArrangedSubview(GroupSettingTagChip(text: $0, compact: false)) }
|
tags.suffix(2).forEach { tagStack.addArrangedSubview(GroupSettingTagChip(text: $0, compact: false)) }
|
||||||
|
tagStack.addArrangedSubview(GroupSettingTagChip(text: "···", compact: true))
|
||||||
} else {
|
} else {
|
||||||
tags.forEach { tagStack.addArrangedSubview(GroupSettingTagChip(text: $0, compact: false)) }
|
tags.forEach { tagStack.addArrangedSubview(GroupSettingTagChip(text: $0, compact: false)) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,13 @@ class GroupView: UIView {
|
||||||
var disposeBag = DisposeBag()
|
var disposeBag = DisposeBag()
|
||||||
|
|
||||||
private let actionCardSpacing: CGFloat = 13
|
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 {
|
private var actionCardHeight: CGFloat {
|
||||||
let availableWidth = UIScreen.main.bounds.width - 32 - actionCardSpacing
|
let availableWidth = UIScreen.main.bounds.width - 32 - actionCardSpacing
|
||||||
|
|
@ -63,7 +70,8 @@ class GroupView: UIView {
|
||||||
segmentView.addSubview(joinedTabLabel)
|
segmentView.addSubview(joinedTabLabel)
|
||||||
segmentView.addSubview(createdTabLabel)
|
segmentView.addSubview(createdTabLabel)
|
||||||
segmentView.addSubview(tabIndicator)
|
segmentView.addSubview(tabIndicator)
|
||||||
listContainer.addSubview(segmentScrollView)
|
listContainer.addSubview(listCardView)
|
||||||
|
listCardView.addSubview(segmentScrollView)
|
||||||
segmentScrollView.addSubview(segmentContentView)
|
segmentScrollView.addSubview(segmentContentView)
|
||||||
// 全部圈子(joined) | 我的圈子(created)
|
// 全部圈子(joined) | 我的圈子(created)
|
||||||
segmentContentView.addSubview(joinedTableView)
|
segmentContentView.addSubview(joinedTableView)
|
||||||
|
|
@ -90,7 +98,7 @@ class GroupView: UIView {
|
||||||
|
|
||||||
circleTabLabel.layoutChain
|
circleTabLabel.layoutChain
|
||||||
.left(20)
|
.left(20)
|
||||||
.bottom(10)
|
.bottom()
|
||||||
.height(28)
|
.height(28)
|
||||||
|
|
||||||
itineraryTabLabel.layoutChain
|
itineraryTabLabel.layoutChain
|
||||||
|
|
@ -160,25 +168,30 @@ class GroupView: UIView {
|
||||||
.centerX(joinedTabLabel)
|
.centerX(joinedTabLabel)
|
||||||
.width(19).height(4)
|
.width(19).height(4)
|
||||||
|
|
||||||
segmentScrollView.layoutChain
|
listCardView.layoutChain
|
||||||
.topToBottomOfView(segmentView)
|
.topToBottomOfView(segmentView, offset: 6)
|
||||||
.edgesHorzontal()
|
.edgesHorzontal(15)
|
||||||
.bottom()
|
.bottom()
|
||||||
.height(kScreenHeight - kNaviHeight - 44 - 12 - actionCardHeight - 16)
|
listHeightConstraint = listCardView.heightAnchor.constraint(equalToConstant: listRowHeight)
|
||||||
|
listHeightConstraint?.isActive = true
|
||||||
|
|
||||||
|
segmentScrollView.layoutChain
|
||||||
|
.edges()
|
||||||
|
|
||||||
segmentContentView.layoutChain
|
segmentContentView.layoutChain
|
||||||
.edges()
|
.edges()
|
||||||
.heightToView(segmentScrollView)
|
.heightToView(segmentScrollView)
|
||||||
|
.widthToView(segmentScrollView, multiplier: 2)
|
||||||
|
|
||||||
let svWidth = UIScreen.main.bounds.width
|
|
||||||
joinedTableView.layoutChain
|
joinedTableView.layoutChain
|
||||||
.top().bottom().left()
|
.top().bottom().left()
|
||||||
.width(svWidth)
|
.widthToView(segmentScrollView)
|
||||||
|
|
||||||
createdTableView.layoutChain
|
createdTableView.layoutChain
|
||||||
.top().bottom()
|
.top().bottom()
|
||||||
.leftToRightOfView(joinedTableView)
|
.leftToRightOfView(joinedTableView)
|
||||||
.width(svWidth)
|
.right()
|
||||||
|
.widthToView(segmentScrollView)
|
||||||
|
|
||||||
// 离屏占位,避免影响布局
|
// 离屏占位,避免影响布局
|
||||||
cycleScrollView.layoutChain
|
cycleScrollView.layoutChain
|
||||||
|
|
@ -192,10 +205,9 @@ class GroupView: UIView {
|
||||||
|
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
segmentContentView.layoutChain.width(bounds.width * 2)
|
guard bounds.size != lastLayoutSize else { return }
|
||||||
listContainer.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
lastLayoutSize = bounds.size
|
||||||
listContainer.layer.cornerRadius = 20
|
updateListHeight(animated: false)
|
||||||
listContainer.clipsToBounds = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Nav
|
// MARK: - Nav
|
||||||
|
|
@ -350,15 +362,23 @@ class GroupView: UIView {
|
||||||
|
|
||||||
// MARK: - List container
|
// MARK: - List container
|
||||||
private lazy var listContainer: UIView = {
|
private lazy var listContainer: UIView = {
|
||||||
|
let v = UIView()
|
||||||
|
v.backgroundColor = .clear
|
||||||
|
return v
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var listCardView: UIView = {
|
||||||
let v = UIView()
|
let v = UIView()
|
||||||
v.backgroundColor = .white
|
v.backgroundColor = .white
|
||||||
|
v.layer.cornerRadius = 10
|
||||||
|
v.clipsToBounds = true
|
||||||
return v
|
return v
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// MARK: - Segment(0 全部圈子=joined,1 我的圈子=created)
|
// MARK: - Segment(0 全部圈子=joined,1 我的圈子=created)
|
||||||
private(set) lazy var segmentView: UIView = {
|
private(set) lazy var segmentView: UIView = {
|
||||||
let v = UIView()
|
let v = UIView()
|
||||||
v.backgroundColor = .white
|
v.backgroundColor = .clear
|
||||||
|
|
||||||
let line = UIView()
|
let line = UIView()
|
||||||
line.backgroundColor = UIColor(hexStr: "#B5B5B5").withAlphaComponent(0.12)
|
line.backgroundColor = UIColor(hexStr: "#B5B5B5").withAlphaComponent(0.12)
|
||||||
|
|
@ -375,7 +395,7 @@ class GroupView: UIView {
|
||||||
private(set) lazy var joinedTabLabel: UILabel = {
|
private(set) lazy var joinedTabLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.text = "全部圈子"
|
label.text = "全部圈子"
|
||||||
label.font = .systemFont(ofSize: 16, weight: .heavy)
|
label.font = .systemFont(ofSize: 16, weight: .bold)
|
||||||
label.textColor = UIColor(hexStr: "#2A3648")
|
label.textColor = UIColor(hexStr: "#2A3648")
|
||||||
label.textAlignment = .center
|
label.textAlignment = .center
|
||||||
label.isUserInteractionEnabled = true
|
label.isUserInteractionEnabled = true
|
||||||
|
|
@ -387,7 +407,7 @@ class GroupView: UIView {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.text = "我的圈子"
|
label.text = "我的圈子"
|
||||||
label.font = .systemFont(ofSize: 16, weight: .medium)
|
label.font = .systemFont(ofSize: 16, weight: .medium)
|
||||||
label.textColor = UIColor(hexStr: "#2A3648")
|
label.textColor = UIColor(hexStr: "#2A3648", alpha: 0.5)
|
||||||
label.textAlignment = .center
|
label.textAlignment = .center
|
||||||
label.isUserInteractionEnabled = true
|
label.isUserInteractionEnabled = true
|
||||||
return label
|
return label
|
||||||
|
|
@ -424,9 +444,9 @@ class GroupView: UIView {
|
||||||
tv.showsVerticalScrollIndicator = false
|
tv.showsVerticalScrollIndicator = false
|
||||||
tv.register(CircleGroupCell.self)
|
tv.register(CircleGroupCell.self)
|
||||||
tv.estimatedRowHeight = 70
|
tv.estimatedRowHeight = 70
|
||||||
tv.rowHeight = 70
|
tv.rowHeight = listRowHeight
|
||||||
tv.bounces = true
|
tv.bounces = false
|
||||||
tv.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 97 + kSafeBottomMargin, right: 0)
|
tv.isScrollEnabled = false
|
||||||
return tv
|
return tv
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -437,9 +457,9 @@ class GroupView: UIView {
|
||||||
tv.showsVerticalScrollIndicator = false
|
tv.showsVerticalScrollIndicator = false
|
||||||
tv.register(CircleGroupCell.self)
|
tv.register(CircleGroupCell.self)
|
||||||
tv.estimatedRowHeight = 70
|
tv.estimatedRowHeight = 70
|
||||||
tv.rowHeight = 70
|
tv.rowHeight = listRowHeight
|
||||||
tv.bounces = true
|
tv.bounces = false
|
||||||
tv.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 97 + kSafeBottomMargin, right: 0)
|
tv.isScrollEnabled = false
|
||||||
return tv
|
return tv
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -450,12 +470,13 @@ class GroupView: UIView {
|
||||||
func selectSegment(at index: Int) {
|
func selectSegment(at index: Int) {
|
||||||
currentSegmentIndex = index
|
currentSegmentIndex = index
|
||||||
let isAll = index == 0
|
let isAll = index == 0
|
||||||
|
updateListHeight(animated: false)
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.2) {
|
UIView.animate(withDuration: 0.2) {
|
||||||
self.joinedTabLabel.textColor = isAll ? UIColor(hexStr: "#2A3648") : UIColor(hexStr: "#2A3648", alpha: 0.5)
|
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.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.tabIndicator.center.x = isAll
|
||||||
? self.joinedTabLabel.center.x
|
? 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) {
|
func selectTopTab(at index: Int) {
|
||||||
guard index == 0 || index == 1 else { return }
|
guard index == 0 || index == 1 else { return }
|
||||||
currentTopTab = index
|
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 转发调用
|
/// 由 GroupViewController 通过 rx.didScroll 转发调用
|
||||||
func handleTableViewScroll(_ scrollView: UIScrollView) {
|
func handleTableViewScroll(_ scrollView: UIScrollView) {
|
||||||
|
guard isMainScrollable else { return }
|
||||||
if isSubCanScroll {
|
if isSubCanScroll {
|
||||||
if scrollView.contentOffset.y <= 0 {
|
if scrollView.contentOffset.y <= 0 {
|
||||||
isSubCanScroll = false
|
isSubCanScroll = false
|
||||||
|
|
@ -526,6 +614,7 @@ extension GroupView: UIScrollViewDelegate {
|
||||||
|
|
||||||
// MARK: - 主 scroll 逻辑
|
// MARK: - 主 scroll 逻辑
|
||||||
private func handleMainScroll(_ scrollView: UIScrollView) {
|
private func handleMainScroll(_ scrollView: UIScrollView) {
|
||||||
|
guard isMainScrollable else { return }
|
||||||
if isSubCanScroll {
|
if isSubCanScroll {
|
||||||
scrollView.contentOffset.y = stickThreshold
|
scrollView.contentOffset.y = stickThreshold
|
||||||
isSticky = true
|
isSticky = true
|
||||||
|
|
@ -673,6 +762,12 @@ final class CircleGroupCell: UITableViewCell {
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
private let separatorView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = UIColor(hexStr: "#EEEEEE")
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
override init(style: CellStyle, reuseIdentifier: String?) {
|
override init(style: CellStyle, reuseIdentifier: String?) {
|
||||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
selectionStyle = .none
|
selectionStyle = .none
|
||||||
|
|
@ -687,6 +782,7 @@ final class CircleGroupCell: UITableViewCell {
|
||||||
contentView.addSubview(timeLabel)
|
contentView.addSubview(timeLabel)
|
||||||
contentView.addSubview(previewLabel)
|
contentView.addSubview(previewLabel)
|
||||||
contentView.addSubview(tagStack)
|
contentView.addSubview(tagStack)
|
||||||
|
contentView.addSubview(separatorView)
|
||||||
|
|
||||||
iconView.layoutChain
|
iconView.layoutChain
|
||||||
.left(15)
|
.left(15)
|
||||||
|
|
@ -720,6 +816,14 @@ final class CircleGroupCell: UITableViewCell {
|
||||||
.topToBottomOfView(nameLabel, offset: 4)
|
.topToBottomOfView(nameLabel, offset: 4)
|
||||||
.leftToView(nameLabel)
|
.leftToView(nameLabel)
|
||||||
.rightToLeftOfView(tagStack, offset: -8)
|
.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) {
|
required init?(coder: NSCoder) {
|
||||||
|
|
@ -731,8 +835,9 @@ final class CircleGroupCell: UITableViewCell {
|
||||||
iconView.setGroupIcon(url: "", fallback: nil)
|
iconView.setGroupIcon(url: "", fallback: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func configure(_ model: GroupCellData) {
|
func configure(_ model: GroupCellData, showsSeparator: Bool) {
|
||||||
let g = model.group
|
let g = model.group
|
||||||
|
separatorView.isHidden = !showsSeparator
|
||||||
iconView.setGroupIcon(url: model.iconURL, fallback: model.localIcon)
|
iconView.setGroupIcon(url: model.iconURL, fallback: model.localIcon)
|
||||||
nameLabel.text = "\(g.name) (\(g.people_no))"
|
nameLabel.text = "\(g.name) (\(g.people_no))"
|
||||||
previewLabel.text = model.lastMsgPreview
|
previewLabel.text = model.lastMsgPreview
|
||||||
|
|
|
||||||
|
|
@ -116,11 +116,19 @@ final class GroupViewController: BaseViewController {
|
||||||
|
|
||||||
viewModel.output.createdSections
|
viewModel.output.createdSections
|
||||||
.observe(on: MainScheduler.asyncInstance)
|
.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))
|
.bind(to: rootView.createdTableView.rx.items(dataSource: createdDataSource))
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
viewModel.output.joinedSections
|
viewModel.output.joinedSections
|
||||||
.observe(on: MainScheduler.asyncInstance)
|
.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))
|
.bind(to: rootView.joinedTableView.rx.items(dataSource: joinedDataSource))
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
|
@ -354,17 +362,21 @@ final class GroupViewController: BaseViewController {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private lazy var createdDataSource: RxTableViewSectionedReloadDataSource<CircleListSectionModel> = {
|
private lazy var createdDataSource: RxTableViewSectionedReloadDataSource<CircleListSectionModel> = {
|
||||||
RxTableViewSectionedReloadDataSource<CircleListSectionModel> { _, tableView, indexPath, model in
|
RxTableViewSectionedReloadDataSource<CircleListSectionModel> { dataSource, tableView, indexPath, model in
|
||||||
let cell: CircleGroupCell = tableView.dequeueReusableCell(for: indexPath)
|
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
|
return cell
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private lazy var joinedDataSource: RxTableViewSectionedReloadDataSource<CircleListSectionModel> = {
|
private lazy var joinedDataSource: RxTableViewSectionedReloadDataSource<CircleListSectionModel> = {
|
||||||
RxTableViewSectionedReloadDataSource<CircleListSectionModel> { _, tableView, indexPath, model in
|
RxTableViewSectionedReloadDataSource<CircleListSectionModel> { dataSource, tableView, indexPath, model in
|
||||||
let cell: CircleGroupCell = tableView.dequeueReusableCell(for: indexPath)
|
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
|
return cell
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ class InviteJoinView: UIView {
|
||||||
inviteCardView.layoutChain
|
inviteCardView.layoutChain
|
||||||
.topToBottomOfView(navView, offset: 42)
|
.topToBottomOfView(navView, offset: 42)
|
||||||
.edgesHorzontal(15)
|
.edgesHorzontal(15)
|
||||||
.heightToWidth(481/345)
|
.heightToWidth(518/345)
|
||||||
|
|
||||||
cardBgImg.layoutChain.edges()
|
cardBgImg.layoutChain.edges()
|
||||||
|
|
||||||
|
|
@ -116,10 +116,10 @@ class InviteJoinView: UIView {
|
||||||
.centerX()
|
.centerX()
|
||||||
|
|
||||||
qrCodeView.layoutChain
|
qrCodeView.layoutChain
|
||||||
.bottom(42)
|
.bottom(38)
|
||||||
.centerX()
|
.centerX()
|
||||||
.width(138)
|
.width(180)
|
||||||
.height(138)
|
.height(180)
|
||||||
|
|
||||||
qrCodeImgView.layoutChain
|
qrCodeImgView.layoutChain
|
||||||
.edges(all: 18)
|
.edges(all: 18)
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,12 @@ final class MailboxView: UIView {
|
||||||
headerGradientLayer.locations = [0, 0.58, 1]
|
headerGradientLayer.locations = [0, 0.58, 1]
|
||||||
headerGradientLayer.startPoint = CGPoint(x: 0, y: 0)
|
headerGradientLayer.startPoint = CGPoint(x: 0, y: 0)
|
||||||
headerGradientLayer.endPoint = CGPoint(x: 1, y: 1)
|
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()
|
setupNavigationBar()
|
||||||
setupCategories()
|
setupCategories()
|
||||||
|
|
@ -253,6 +258,13 @@ final class MailboxView: UIView {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
@objc private func categoryTapped(_ sender: MailboxCategoryView) {
|
||||||
onSelectCategory?(sender.category)
|
onSelectCategory?(sender.category)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,31 @@ final class AppUsageReportVC: BaseViewController {
|
||||||
self?.rootView.apply(snapshot)
|
self?.rootView.apply(snapshot)
|
||||||
})
|
})
|
||||||
.disposed(by: disposeBag)
|
.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,14 @@ final class AppUsageReportView: UIView {
|
||||||
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
view.backgroundColor = .white
|
view.backgroundColor = .white
|
||||||
view.showsHorizontalScrollIndicator = false
|
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.register(GroupMemberListCell.self)
|
||||||
view.applyAppUsageReportCardStyle(cornerRadius: 20)
|
view.applyAppUsageReportCardStyle(cornerRadius: 20)
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var onPeriodChanged: ((AppUsageReportPeriod) -> Void)?
|
var onPeriodChanged: ((AppUsageReportPeriod) -> Void)?
|
||||||
|
var onRemind: (() -> Void)?
|
||||||
|
|
||||||
private let navBgView = UIImageView(image: UIImage(named: "Common/navBar_bg_2"))
|
private let navBgView = UIImageView(image: UIImage(named: "Common/navBar_bg_2"))
|
||||||
private let scrollView = UIScrollView()
|
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() {
|
private func bindPeriodControls() {
|
||||||
let onChange: (AppUsageReportPeriod) -> Void = { [weak self] period in
|
let onChange: (AppUsageReportPeriod) -> Void = { [weak self] period in
|
||||||
self?.onPeriodChanged?(period)
|
self?.onPeriodChanged?(period)
|
||||||
|
|
@ -142,6 +149,7 @@ private final class AppUsageFrequentSectionView: UIView {
|
||||||
private let periodControl = AppUsagePeriodControl()
|
private let periodControl = AppUsagePeriodControl()
|
||||||
private let rowsStack = UIStackView()
|
private let rowsStack = UIStackView()
|
||||||
private let statusLab = UILabel()
|
private let statusLab = UILabel()
|
||||||
|
private let deniedOverlay = PrivilegeDeniedOverlay()
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
@ -214,6 +222,22 @@ private final class AppUsageFrequentSectionView: UIView {
|
||||||
statusLab.numberOfLines = 0
|
statusLab.numberOfLines = 0
|
||||||
cardView.addSubview(statusLab)
|
cardView.addSubview(statusLab)
|
||||||
statusLab.layoutChain.centerX().centerY(cardView, offset: 16).left(24).right(24)
|
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 totalHintLab = UILabel()
|
||||||
private let legendStack = UIStackView()
|
private let legendStack = UIStackView()
|
||||||
private let statusLab = UILabel()
|
private let statusLab = UILabel()
|
||||||
|
private let deniedOverlay = PrivilegeDeniedOverlay()
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
@ -336,6 +361,22 @@ private final class AppUsageDurationSectionView: UIView {
|
||||||
statusLab.numberOfLines = 0
|
statusLab.numberOfLines = 0
|
||||||
cardView.addSubview(statusLab)
|
cardView.addSubview(statusLab)
|
||||||
statusLab.layoutChain.centerX().centerY(cardView, offset: 18).left(24).right(24)
|
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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ final class AppUsageReportViewModel {
|
||||||
let selectedPeriod = BehaviorRelay<AppUsageReportPeriod>(value: .week)
|
let selectedPeriod = BehaviorRelay<AppUsageReportPeriod>(value: .week)
|
||||||
let snapshot = BehaviorRelay<Snapshot>(value: .init(state: .loading, period: .week, totalUsageTime: 0, apps: []))
|
let snapshot = BehaviorRelay<Snapshot>(value: .init(state: .loading, period: .week, totalUsageTime: 0, apps: []))
|
||||||
|
|
||||||
private let groupKey: String
|
let groupKey: String
|
||||||
private let disposeBag = DisposeBag()
|
private let disposeBag = DisposeBag()
|
||||||
private var reportCache: [String: PhoneUsagePeriodModel] = [:]
|
private var reportCache: [String: PhoneUsagePeriodModel] = [:]
|
||||||
private var requestIDs: [String: UUID] = [:]
|
private var requestIDs: [String: UUID] = [:]
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@ class GroupListPopView: UIView {
|
||||||
private func updateTableHeight() {
|
private func updateTableHeight() {
|
||||||
let visible = min(groupList.count, Self.maxVisibleRows)
|
let visible = min(groupList.count, Self.maxVisibleRows)
|
||||||
let height = CGFloat(visible) * Self.rowHeight
|
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.isScrollEnabled = groupList.count > Self.maxVisibleRows
|
||||||
tableView.reloadData()
|
tableView.reloadData()
|
||||||
}
|
}
|
||||||
|
|
@ -315,11 +315,8 @@ class GroupListCell: UITableViewCell {
|
||||||
cardView.layer.borderColor = GroupListPopView.primaryBlueColor.cgColor
|
cardView.layer.borderColor = GroupListPopView.primaryBlueColor.cgColor
|
||||||
|
|
||||||
checkIcon.image = isSelected
|
checkIcon.image = isSelected
|
||||||
? UIImage(systemName: "checkmark.circle.fill")
|
? UIImage(named: "Common/checkbox_on")
|
||||||
: UIImage(systemName: "circle")
|
: UIImage(named: "Common/checkbox")
|
||||||
checkIcon.tintColor = isSelected
|
|
||||||
? GroupListPopView.primaryBlueColor
|
|
||||||
: UIColor(hexStr: "#D0D0D0")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func makeTagLabel(_ text: String) -> UILabel {
|
private static func makeTagLabel(_ text: String) -> UILabel {
|
||||||
|
|
@ -365,8 +362,8 @@ class GroupListCell: UITableViewCell {
|
||||||
checkIcon.layoutChain
|
checkIcon.layoutChain
|
||||||
.right(12)
|
.right(12)
|
||||||
.centerY()
|
.centerY()
|
||||||
.width(22)
|
.width(24)
|
||||||
.height(22)
|
.height(24)
|
||||||
|
|
||||||
nameLab.layoutChain
|
nameLab.layoutChain
|
||||||
.topToView(avaterImgView, offset: 2)
|
.topToView(avaterImgView, offset: 2)
|
||||||
|
|
@ -379,7 +376,7 @@ class GroupListCell: UITableViewCell {
|
||||||
.rightToLeftOfView(checkIcon, offset: -8, relation: .lessThanOrEqual)
|
.rightToLeftOfView(checkIcon, offset: -8, relation: .lessThanOrEqual)
|
||||||
|
|
||||||
countLab.layoutChain
|
countLab.layoutChain
|
||||||
.topToBottomOfView(nameLab, offset: 4)
|
.bottomToView(avaterImgView)
|
||||||
.leftToView(nameLab)
|
.leftToView(nameLab)
|
||||||
.rightToLeftOfView(checkIcon, offset: -8)
|
.rightToLeftOfView(checkIcon, offset: -8)
|
||||||
}
|
}
|
||||||
|
|
@ -437,7 +434,7 @@ class GroupListCell: UITableViewCell {
|
||||||
|
|
||||||
lazy var checkIcon: UIImageView = {
|
lazy var checkIcon: UIImageView = {
|
||||||
let view = UIImageView()
|
let view = UIImageView()
|
||||||
view.contentMode = .scaleAspectFit
|
view.contentMode = .scaleAspectFill
|
||||||
view.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)
|
view.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,13 @@ class GroupMemberView2: UIView {
|
||||||
private var messageEchoByUserId: [String: String] = [:]
|
private var messageEchoByUserId: [String: String] = [:]
|
||||||
private var phoneStatusObserver: NSObjectProtocol?
|
private var phoneStatusObserver: NSObjectProtocol?
|
||||||
private var weatherRequestID = UUID()
|
private var weatherRequestID = UUID()
|
||||||
|
private var localWeatherSnapshot: MemberWeatherSnapshot?
|
||||||
|
private var localWeatherSnapshotKey: String?
|
||||||
|
private var localWeatherUpdatedAt: Date?
|
||||||
|
private var pendingWeatherLocationKey: String?
|
||||||
var onCoupleMemberTap: ((GroupMemberModel) -> Void)?
|
var onCoupleMemberTap: ((GroupMemberModel) -> Void)?
|
||||||
|
|
||||||
|
private static let weatherRefreshInterval: TimeInterval = 15 * 60
|
||||||
private static let batteryNormalColor = UIColor(hexStr: "#47DD00")
|
private static let batteryNormalColor = UIColor(hexStr: "#47DD00")
|
||||||
private static let batteryLowColor = UIColor(hexStr: "#F85A5D")
|
private static let batteryLowColor = UIColor(hexStr: "#F85A5D")
|
||||||
private static let batteryDefaultTextColor = UIColor(hexStr: "#353B4F")
|
private static let batteryDefaultTextColor = UIColor(hexStr: "#353B4F")
|
||||||
|
|
@ -45,10 +50,11 @@ class GroupMemberView2: UIView {
|
||||||
ownView.isHidden = !isOwner
|
ownView.isHidden = !isOwner
|
||||||
relationIconView.configure(relationIdx: model.extra.relation_idx)
|
relationIconView.configure(relationIdx: model.extra.relation_idx)
|
||||||
updateMoodBadge(model.mood)
|
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 {
|
if !model.is_online, !isCurrentUser {
|
||||||
locationLab.text = "**"
|
locationLab.text = "**"
|
||||||
distanceView.isHidden = true
|
distanceView.isHidden = true
|
||||||
|
|
@ -59,6 +65,8 @@ class GroupMemberView2: UIView {
|
||||||
|
|
||||||
applyPhoneUsage(phoneUsage)
|
applyPhoneUsage(phoneUsage)
|
||||||
if isCurrentUser {
|
if isCurrentUser {
|
||||||
|
applyLocalDeviceInfo()
|
||||||
|
startLocalPhoneStatusObserving()
|
||||||
requestLocalWeather()
|
requestLocalWeather()
|
||||||
}
|
}
|
||||||
updateRealtimeInteractionVisibility(isCurrentUser: isCurrentUser)
|
updateRealtimeInteractionVisibility(isCurrentUser: isCurrentUser)
|
||||||
|
|
@ -84,6 +92,14 @@ class GroupMemberView2: UIView {
|
||||||
func updatePhoneUsage(_ phoneUsage: PhoneUsageTodayModel?, for userId: String) {
|
func updatePhoneUsage(_ phoneUsage: PhoneUsageTodayModel?, for userId: String) {
|
||||||
guard currentMemberModel?.user_id == userId else { return }
|
guard currentMemberModel?.user_id == userId else { return }
|
||||||
applyPhoneUsage(phoneUsage)
|
applyPhoneUsage(phoneUsage)
|
||||||
|
if Self.isCurrentUser(userId) {
|
||||||
|
applyLocalDeviceInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshLocalDeviceInfoIfNeeded() {
|
||||||
|
guard isShowingCurrentUser else { return }
|
||||||
|
applyLocalDeviceInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyEmojiEcho(name: String, for userId: String) {
|
func applyEmojiEcho(name: String, for userId: String) {
|
||||||
|
|
@ -223,8 +239,13 @@ class GroupMemberView2: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isShowingCurrentUser: Bool {
|
private var isShowingCurrentUser: Bool {
|
||||||
guard let userId = currentMemberModel?.user_id, !userId.isEmpty else { return false }
|
Self.isCurrentUser(currentMemberModel?.user_id)
|
||||||
return userId == AppContextManager.shared.userId
|
}
|
||||||
|
|
||||||
|
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 {
|
private var isShowingOfflineOther: Bool {
|
||||||
|
|
@ -270,6 +291,7 @@ class GroupMemberView2: UIView {
|
||||||
self.phoneStatusObserver = nil
|
self.phoneStatusObserver = nil
|
||||||
}
|
}
|
||||||
weatherRequestID = UUID()
|
weatherRequestID = UUID()
|
||||||
|
pendingWeatherLocationKey = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applyLocalDeviceInfo() {
|
private func applyLocalDeviceInfo() {
|
||||||
|
|
@ -287,30 +309,64 @@ class GroupMemberView2: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func requestLocalWeather() {
|
private func requestLocalWeather() {
|
||||||
let requestID = UUID()
|
|
||||||
weatherRequestID = requestID
|
|
||||||
applyUnknownWeather(text: "***")
|
|
||||||
guard let latitude = Defaults[\.currentLatitude],
|
guard let latitude = Defaults[\.currentLatitude],
|
||||||
let longitude = Defaults[\.currentLongitude] else {
|
let longitude = Defaults[\.currentLongitude] else {
|
||||||
|
if localWeatherSnapshot == nil {
|
||||||
applyUnknownWeather(text: "***")
|
applyUnknownWeather(text: "***")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let location = CLLocation(latitude: latitude, longitude: longitude)
|
let location = CLLocation(latitude: latitude, longitude: longitude)
|
||||||
guard CLLocationCoordinate2DIsValid(location.coordinate) else {
|
guard CLLocationCoordinate2DIsValid(location.coordinate) else {
|
||||||
|
if localWeatherSnapshot == nil {
|
||||||
applyUnknownWeather(text: "***")
|
applyUnknownWeather(text: "***")
|
||||||
|
}
|
||||||
return
|
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
|
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 {
|
switch result {
|
||||||
case .success(let snapshot):
|
case .success(let snapshot):
|
||||||
self.weatherIcon.image = UIImage(named: snapshot.assetName)
|
self.localWeatherSnapshot = snapshot
|
||||||
self.weatherLab.text = snapshot.text
|
self.localWeatherSnapshotKey = locationKey
|
||||||
|
self.localWeatherUpdatedAt = Date()
|
||||||
|
self.applyWeather(snapshot)
|
||||||
case .failure:
|
case .failure:
|
||||||
|
if self.localWeatherSnapshot == nil {
|
||||||
self.applyUnknownWeather(text: "***")
|
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) {
|
private func updateAppUsageVisibility(_ visible: Bool) {
|
||||||
if visible {
|
if visible {
|
||||||
|
|
@ -345,8 +401,7 @@ class GroupMemberView2: UIView {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let snapshot = MemberWeatherService.snapshot(reportedText: weather)
|
let snapshot = MemberWeatherService.snapshot(reportedText: weather)
|
||||||
weatherIcon.image = UIImage(named: snapshot.assetName)
|
applyWeather(snapshot)
|
||||||
weatherLab.text = snapshot.text
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applyUnknownWeather(text: String) {
|
private func applyUnknownWeather(text: String) {
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,7 @@ class HomeView2: UIView {
|
||||||
.centerY()
|
.centerY()
|
||||||
|
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.text = "有权限未开启,数据可能会异常"
|
label.text = "定位权限未开启,数据可能会异常"
|
||||||
label.textColor = UIColor(hexStr: "#353B4F")
|
label.textColor = UIColor(hexStr: "#353B4F")
|
||||||
label.font = .systemFont(ofSize: 12, weight: .medium)
|
label.font = .systemFont(ofSize: 12, weight: .medium)
|
||||||
view.addSubview(label)
|
view.addSubview(label)
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,7 @@ class HomeViewController: BaseViewController {
|
||||||
super.viewDidAppear(animated)
|
super.viewDidAppear(animated)
|
||||||
UnlockCountManager.shared.refreshAuthorizationAndMonitoring()
|
UnlockCountManager.shared.refreshAuthorizationAndMonitoring()
|
||||||
MQTTService.shared.applicationDidBecomeActive()
|
MQTTService.shared.applicationDidBecomeActive()
|
||||||
|
rootView.groupMemberView.refreshLocalDeviceInfoIfNeeded()
|
||||||
|
|
||||||
let manager = AuthorizeManager.manager(type: .locationAlways)
|
let manager = AuthorizeManager.manager(type: .locationAlways)
|
||||||
rootView.warningView.isHidden = manager?.authorizeStatus() == .authorized
|
rootView.warningView.isHidden = manager?.authorizeStatus() == .authorized
|
||||||
|
|
@ -353,11 +354,11 @@ class HomeViewController: BaseViewController {
|
||||||
self.openAppUsageReport()
|
self.openAppUsageReport()
|
||||||
}).disposed(by: disposeBag)
|
}).disposed(by: disposeBag)
|
||||||
|
|
||||||
rootView.groupMemberView.onPhoneReportRemind = {
|
rootView.groupMemberView.onPhoneReportRemind = { [weak self] in
|
||||||
DLToast.show(text: "已提醒")
|
self?.remindSelectedMember(content: "请开启手机报告隐私设置")
|
||||||
}
|
}
|
||||||
rootView.groupMemberView.onAppUsageRemind = {
|
rootView.groupMemberView.onAppUsageRemind = { [weak self] in
|
||||||
DLToast.show(text: "已提醒")
|
self?.remindSelectedMember(content: "请开启App使用记录隐私设置")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 地图回到自己并重新开启自动跟随
|
// 地图回到自己并重新开启自动跟随
|
||||||
|
|
@ -1150,6 +1151,14 @@ class HomeViewController: BaseViewController {
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
private func remindSelectedMember(content: String) {
|
||||||
|
GroupService.sendPrivilegeRemind(
|
||||||
|
content: content,
|
||||||
|
groupKey: viewModel.groupModel?.default_group_key ?? "",
|
||||||
|
userId: selectedMemberId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// 若更新的是当前选中成员,刷新信息卡(位置 / 在线 / 距离等)
|
/// 若更新的是当前选中成员,刷新信息卡(位置 / 在线 / 距离等)
|
||||||
private func refreshSelectedMemberInfoIfNeeded(userId: String) {
|
private func refreshSelectedMemberInfoIfNeeded(userId: String) {
|
||||||
guard userId == selectedMemberId,
|
guard userId == selectedMemberId,
|
||||||
|
|
@ -1537,6 +1546,13 @@ extension HomeViewController {
|
||||||
self?.removeAnnotation(userId: userId)
|
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": // 求助
|
case "sos": // 求助
|
||||||
guard let userId = msg.data?.user_id,
|
guard let userId = msg.data?.user_id,
|
||||||
userId != AppContextManager.shared.userId,
|
userId != AppContextManager.shared.userId,
|
||||||
|
|
@ -1547,6 +1563,13 @@ extension HomeViewController {
|
||||||
guard let member = self.viewModel.memberList.first(where: { $0.user_id == userId }) else { return }
|
guard let member = self.viewModel.memberList.first(where: { $0.user_id == userId }) else { return }
|
||||||
self.showSOSReceiveAlert(for: member)
|
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":
|
case "join", "leave", "dismiss", "kick":
|
||||||
break
|
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() {
|
private func updateOnlineCount() {
|
||||||
let total = viewModel.memberCount
|
let total = viewModel.memberCount
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,9 @@ class HomeViewModel {
|
||||||
|
|
||||||
// 是否当前用户
|
// 是否当前用户
|
||||||
func isCurrentUser(id: String) -> Bool {
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// 是否圈主
|
// 是否圈主
|
||||||
|
|
|
||||||
|
|
@ -612,7 +612,7 @@ final class EmojiPanelCell: UICollectionViewCell {
|
||||||
self.lottieView.play()
|
self.lottieView.play()
|
||||||
}
|
}
|
||||||
|
|
||||||
lockView.isHidden = AppContextManager.shared.vip > 1
|
lockView.isHidden = true//AppContextManager.shared.vip > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func playAnimation() {
|
func playAnimation() {
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ class LocationPermissionPopView: UIView {
|
||||||
lazy var popBgView: UIView = {
|
lazy var popBgView: UIView = {
|
||||||
let view = UIView()
|
let view = UIView()
|
||||||
view.backgroundColor = .white
|
view.backgroundColor = .white
|
||||||
view.cornerRadius = 30
|
view.cornerRadius = 40
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,8 @@ final class MemberAppUsageView: UIView {
|
||||||
deniedOverlay.configure(
|
deniedOverlay.configure(
|
||||||
title: "TA未开启设置",
|
title: "TA未开启设置",
|
||||||
message: "无法查看app使用记录",
|
message: "无法查看app使用记录",
|
||||||
actionTitle: "提醒TA"
|
actionTitle: "提醒TA",
|
||||||
|
imageName: "PrivilegeDenied/app_usage"
|
||||||
)
|
)
|
||||||
deniedOverlay.onAction = { [weak self] in self?.onDeniedAction?() }
|
deniedOverlay.onAction = { [weak self] in self?.onDeniedAction?() }
|
||||||
deniedOverlay.setVisible(denied)
|
deniedOverlay.setVisible(denied)
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,8 @@ final class MemberPhoneReportView: UIView {
|
||||||
deniedOverlay.configure(
|
deniedOverlay.configure(
|
||||||
title: "TA未开启设置",
|
title: "TA未开启设置",
|
||||||
message: "无法查看手机报告",
|
message: "无法查看手机报告",
|
||||||
actionTitle: "提醒TA"
|
actionTitle: "提醒TA",
|
||||||
|
imageName: "PrivilegeDenied/phone_report"
|
||||||
)
|
)
|
||||||
deniedOverlay.onAction = { [weak self] in self?.onDeniedAction?() }
|
deniedOverlay.onAction = { [weak self] in self?.onDeniedAction?() }
|
||||||
deniedOverlay.setVisible(denied)
|
deniedOverlay.setVisible(denied)
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,9 @@ final class PhoneReportDetailVC: BaseViewController {
|
||||||
rootView.memberCV.delegate = self
|
rootView.memberCV.delegate = self
|
||||||
bind()
|
bind()
|
||||||
rootView.memberCV.reloadData()
|
rootView.memberCV.reloadData()
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.rootView.updateMemberFadeVisibility()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func bind() {
|
private func bind() {
|
||||||
|
|
@ -80,6 +83,30 @@ final class PhoneReportDetailVC: BaseViewController {
|
||||||
})
|
})
|
||||||
.disposed(by: disposeBag)
|
.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() {
|
private func switchGroup() {
|
||||||
|
|
@ -114,4 +141,9 @@ extension PhoneReportDetailVC: UICollectionViewDataSource, UICollectionViewDeleg
|
||||||
let model = viewModel.members[indexPath.item]
|
let model = viewModel.members[indexPath.item]
|
||||||
viewModel.selectedMemberId.accept(model.user_id)
|
viewModel.selectedMemberId.accept(model.user_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
guard scrollView === rootView.memberCV else { return }
|
||||||
|
rootView.updateMemberFadeVisibility()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,16 +37,17 @@ final class PhoneReportDetailView: UIView {
|
||||||
layout.itemSize = CGSize(width: 61, height: 90)
|
layout.itemSize = CGSize(width: 61, height: 90)
|
||||||
layout.minimumLineSpacing = 12
|
layout.minimumLineSpacing = 12
|
||||||
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
cv.backgroundColor = .white
|
cv.backgroundColor = .clear
|
||||||
cv.showsHorizontalScrollIndicator = false
|
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.register(GroupMemberListCell.self)
|
||||||
cv.applyPhoneReportCardStyle(cornerRadius: 20)
|
// cv.applyPhoneReportCardStyle(cornerRadius: 20)
|
||||||
return cv
|
return cv
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let scrollView = UIScrollView()
|
let scrollView = UIScrollView()
|
||||||
let contentView = UIView()
|
let contentView = UIView()
|
||||||
|
private let memberTrailingFadeView = PhoneReportMemberTrailingFadeView()
|
||||||
|
|
||||||
// MARK: Status
|
// MARK: Status
|
||||||
let statusTitleIcon = UIImageView(image: UIImage(named: "Home/report_phone"))
|
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 deviceImage = UIImageView(image: UIImage(named: "Home/phone_device"))
|
||||||
let brandLogoImage = UIImageView()
|
let brandLogoImage = UIImageView()
|
||||||
private var brandLogoURL = ""
|
private var brandLogoURL = ""
|
||||||
|
private let statusDeniedOverlay = PrivilegeDeniedOverlay()
|
||||||
|
var onRemind: (() -> Void)?
|
||||||
|
|
||||||
// MARK: Charts
|
// MARK: Charts
|
||||||
let screenSection = PhoneReportChartSectionView(
|
let screenSection = PhoneReportChartSectionView(
|
||||||
|
|
@ -74,7 +77,8 @@ final class PhoneReportDetailView: UIView {
|
||||||
summaryColor: UIColor(hexStr: "#00ADFE"),
|
summaryColor: UIColor(hexStr: "#00ADFE"),
|
||||||
barGradient: (UIColor(hexStr: "#C0EAFF"), UIColor(hexStr: "#ECF9FF")),
|
barGradient: (UIColor(hexStr: "#C0EAFF"), UIColor(hexStr: "#ECF9FF")),
|
||||||
highlightGradient: (UIColor(hexStr: "#16B3FF"), UIColor(hexStr: "#9EDFFF")),
|
highlightGradient: (UIColor(hexStr: "#16B3FF"), UIColor(hexStr: "#9EDFFF")),
|
||||||
tipColor: UIColor(hexStr: "#20B6FF")
|
tipColor: UIColor(hexStr: "#20B6FF"),
|
||||||
|
tipArrowImageName: "Home/report_screen_tip_arrow"
|
||||||
)
|
)
|
||||||
let unlockSection = PhoneReportChartSectionView(
|
let unlockSection = PhoneReportChartSectionView(
|
||||||
title: "APP使用次数",
|
title: "APP使用次数",
|
||||||
|
|
@ -84,6 +88,7 @@ final class PhoneReportDetailView: UIView {
|
||||||
barGradient: (UIColor(hexStr: "#DDFFA1"), UIColor(hexStr: "#F2FFDC")),
|
barGradient: (UIColor(hexStr: "#DDFFA1"), UIColor(hexStr: "#F2FFDC")),
|
||||||
highlightGradient: (UIColor(hexStr: "#AAF52A"), UIColor(hexStr: "#E8FFC1")),
|
highlightGradient: (UIColor(hexStr: "#AAF52A"), UIColor(hexStr: "#E8FFC1")),
|
||||||
tipColor: UIColor(hexStr: "#86E73D"),
|
tipColor: UIColor(hexStr: "#86E73D"),
|
||||||
|
tipArrowImageName: "Home/report_usage_tip_arrow",
|
||||||
valueSuffix: "次"
|
valueSuffix: "次"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -118,12 +123,29 @@ final class PhoneReportDetailView: UIView {
|
||||||
scrollView.addSubview(contentView)
|
scrollView.addSubview(contentView)
|
||||||
contentView.layoutChain.edges().widthToView(scrollView)
|
contentView.layoutChain.edges().widthToView(scrollView)
|
||||||
|
|
||||||
contentView.addSubview(memberCV)
|
let memberView = UIView()
|
||||||
memberCV.layoutChain
|
memberView.backgroundColor = .white
|
||||||
|
memberView.applyPhoneReportCardStyle(cornerRadius: 20)
|
||||||
|
|
||||||
|
contentView.addSubview(memberView)
|
||||||
|
memberView.addSubview(memberCV)
|
||||||
|
memberView.addSubview(memberTrailingFadeView)
|
||||||
|
|
||||||
|
memberView.layoutChain
|
||||||
.top(8)
|
.top(8)
|
||||||
.edgesHorzontal(15)
|
.edgesHorzontal(15)
|
||||||
.height(100)
|
.height(100)
|
||||||
|
|
||||||
|
memberCV.layoutChain
|
||||||
|
.edgesVertical()
|
||||||
|
.edgesHorzontal()
|
||||||
|
|
||||||
|
memberTrailingFadeView.layoutChain
|
||||||
|
.top()
|
||||||
|
.right()
|
||||||
|
.bottom()
|
||||||
|
.width(52)
|
||||||
|
|
||||||
statusTitleIcon.contentMode = .scaleAspectFit
|
statusTitleIcon.contentMode = .scaleAspectFit
|
||||||
contentView.addSubview(statusTitleIcon)
|
contentView.addSubview(statusTitleIcon)
|
||||||
statusTitleIcon.layoutChain
|
statusTitleIcon.layoutChain
|
||||||
|
|
@ -147,6 +169,9 @@ final class PhoneReportDetailView: UIView {
|
||||||
.topToBottomOfView(statusTitleIcon, offset: 10)
|
.topToBottomOfView(statusTitleIcon, offset: 10)
|
||||||
.edgesHorzontal(15)
|
.edgesHorzontal(15)
|
||||||
.height(175)
|
.height(175)
|
||||||
|
statusCard.addSubview(statusDeniedOverlay)
|
||||||
|
statusDeniedOverlay.layoutChain.edges()
|
||||||
|
statusDeniedOverlay.layer.cornerRadius = 20
|
||||||
|
|
||||||
let leftPanel = UIView()
|
let leftPanel = UIView()
|
||||||
leftPanel.backgroundColor = .clear
|
leftPanel.backgroundColor = .clear
|
||||||
|
|
@ -182,7 +207,7 @@ final class PhoneReportDetailView: UIView {
|
||||||
.right(12)
|
.right(12)
|
||||||
.height(64)
|
.height(64)
|
||||||
|
|
||||||
deviceCard.backgroundColor = UIColor(hexStr: "#F7FBFF")
|
deviceCard.backgroundColor = UIColor(hexStr: "#FAFAFA")
|
||||||
deviceCard.cornerRadius = 12
|
deviceCard.cornerRadius = 12
|
||||||
statusCard.addSubview(deviceCard)
|
statusCard.addSubview(deviceCard)
|
||||||
deviceCard.layoutChain
|
deviceCard.layoutChain
|
||||||
|
|
@ -240,6 +265,21 @@ final class PhoneReportDetailView: UIView {
|
||||||
.bottom(24)
|
.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) {
|
func apply(_ snap: PhoneReportDetailViewModel.StatusSnapshot) {
|
||||||
batteryRing.configure(percent: snap.batteryPercent, text: snap.batteryText, isCharging: snap.isCharging)
|
batteryRing.configure(percent: snap.batteryPercent, text: snap.batteryText, isCharging: snap.isCharging)
|
||||||
estimateLab.text = "预计使用\(snap.estimateText)"
|
estimateLab.text = "预计使用\(snap.estimateText)"
|
||||||
|
|
@ -256,6 +296,27 @@ final class PhoneReportDetailView: UIView {
|
||||||
unlockSection.setBars(snap.appUsageBars)
|
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) {
|
private func updateBrandLogo(urlString: String) {
|
||||||
brandLogoURL = urlString
|
brandLogoURL = urlString
|
||||||
brandLogoImage.kf.cancelDownloadTask()
|
brandLogoImage.kf.cancelDownloadTask()
|
||||||
|
|
@ -284,10 +345,10 @@ private extension UIView {
|
||||||
layer.cornerRadius = cornerRadius
|
layer.cornerRadius = cornerRadius
|
||||||
clipsToBounds = false
|
clipsToBounds = false
|
||||||
layer.masksToBounds = false
|
layer.masksToBounds = false
|
||||||
layer.shadowColor = UIColor(hexStr: "#0F2846", alpha: 0.1).cgColor
|
layer.shadowColor = UIColor(hexStr: "#E5E5E5", alpha: 0.3).cgColor
|
||||||
layer.shadowOffset = CGSize(width: 0, height: 2)
|
layer.shadowOffset = CGSize(width: 0, height: 0)
|
||||||
layer.shadowOpacity = 1
|
layer.shadowOpacity = 1
|
||||||
layer.shadowRadius = 10
|
layer.shadowRadius = 5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -351,7 +412,7 @@ final class PhoneStatusMiniCard: UIView {
|
||||||
|
|
||||||
init(iconName: String, title: String) {
|
init(iconName: String, title: String) {
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
backgroundColor = UIColor(hexStr: "#F7FBFF")
|
backgroundColor = UIColor(hexStr: "#FAFAFA")
|
||||||
cornerRadius = 12
|
cornerRadius = 12
|
||||||
icon.image = UIImage(named: iconName)
|
icon.image = UIImage(named: iconName)
|
||||||
icon.contentMode = .scaleAspectFit
|
icon.contentMode = .scaleAspectFit
|
||||||
|
|
@ -387,7 +448,10 @@ final class PhoneReportChartSectionView: UIView {
|
||||||
private let barGradient: (UIColor, UIColor)
|
private let barGradient: (UIColor, UIColor)
|
||||||
private let highlightGradient: (UIColor, UIColor)
|
private let highlightGradient: (UIColor, UIColor)
|
||||||
private let tipColor: UIColor
|
private let tipColor: UIColor
|
||||||
|
private let tipArrowImageName: String
|
||||||
private let valueSuffix: String
|
private let valueSuffix: String
|
||||||
|
private let card = UIView()
|
||||||
|
private let deniedOverlay = PrivilegeDeniedOverlay()
|
||||||
|
|
||||||
init(
|
init(
|
||||||
title: String,
|
title: String,
|
||||||
|
|
@ -397,6 +461,7 @@ final class PhoneReportChartSectionView: UIView {
|
||||||
barGradient: (UIColor, UIColor),
|
barGradient: (UIColor, UIColor),
|
||||||
highlightGradient: (UIColor, UIColor),
|
highlightGradient: (UIColor, UIColor),
|
||||||
tipColor: UIColor,
|
tipColor: UIColor,
|
||||||
|
tipArrowImageName: String,
|
||||||
valueSuffix: String = ""
|
valueSuffix: String = ""
|
||||||
) {
|
) {
|
||||||
self.summaryPrefix = summaryPrefix
|
self.summaryPrefix = summaryPrefix
|
||||||
|
|
@ -404,6 +469,7 @@ final class PhoneReportChartSectionView: UIView {
|
||||||
self.barGradient = barGradient
|
self.barGradient = barGradient
|
||||||
self.highlightGradient = highlightGradient
|
self.highlightGradient = highlightGradient
|
||||||
self.tipColor = tipColor
|
self.tipColor = tipColor
|
||||||
|
self.tipArrowImageName = tipArrowImageName
|
||||||
self.valueSuffix = valueSuffix
|
self.valueSuffix = valueSuffix
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
let titleIcon = UIImageView(image: UIImage(named: iconName))
|
let titleIcon = UIImageView(image: UIImage(named: iconName))
|
||||||
|
|
@ -416,11 +482,13 @@ final class PhoneReportChartSectionView: UIView {
|
||||||
addSubview(titleLab)
|
addSubview(titleLab)
|
||||||
titleLab.layoutChain.centerY(titleIcon).leftToRightOfView(titleIcon, offset: 6)
|
titleLab.layoutChain.centerY(titleIcon).leftToRightOfView(titleIcon, offset: 6)
|
||||||
|
|
||||||
let card = UIView()
|
|
||||||
card.backgroundColor = .white
|
card.backgroundColor = .white
|
||||||
card.applyPhoneReportCardStyle(cornerRadius: 20)
|
card.applyPhoneReportCardStyle(cornerRadius: 20)
|
||||||
addSubview(card)
|
addSubview(card)
|
||||||
card.layoutChain.topToBottomOfView(titleLab, offset: 10).edgesHorzontal().bottom().height(180)
|
card.layoutChain.topToBottomOfView(titleLab, offset: 10).edgesHorzontal().bottom().height(180)
|
||||||
|
card.addSubview(deniedOverlay)
|
||||||
|
deniedOverlay.layoutChain.edges()
|
||||||
|
deniedOverlay.layer.cornerRadius = 20
|
||||||
|
|
||||||
summaryPrefixLab.text = summaryPrefix
|
summaryPrefixLab.text = summaryPrefix
|
||||||
summaryPrefixLab.font = .systemFont(ofSize: 12, weight: .medium)
|
summaryPrefixLab.font = .systemFont(ofSize: 12, weight: .medium)
|
||||||
|
|
@ -436,7 +504,7 @@ final class PhoneReportChartSectionView: UIView {
|
||||||
summaryLab.layoutChain
|
summaryLab.layoutChain
|
||||||
.centerY(summaryPrefixLab)
|
.centerY(summaryPrefixLab)
|
||||||
.leftToRightOfView(summaryPrefixLab, offset: 4)
|
.leftToRightOfView(summaryPrefixLab, offset: 4)
|
||||||
.right(14, relation: .lessThanOrEqual)
|
.right(14, relation: .greaterThanOrEqual)
|
||||||
|
|
||||||
card.addSubview(chart)
|
card.addSubview(chart)
|
||||||
chart.layoutChain.topToBottomOfView(summaryPrefixLab, offset: 16).edgesHorzontal(25).bottom(14)
|
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") }
|
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) {
|
func setSummary(_ text: String) {
|
||||||
summaryLab.text = text
|
summaryLab.text = text
|
||||||
}
|
}
|
||||||
|
|
@ -454,6 +534,7 @@ final class PhoneReportChartSectionView: UIView {
|
||||||
barGradient: barGradient,
|
barGradient: barGradient,
|
||||||
highlightGradient: highlightGradient,
|
highlightGradient: highlightGradient,
|
||||||
tipColor: tipColor,
|
tipColor: tipColor,
|
||||||
|
tipArrowImageName: tipArrowImageName,
|
||||||
suffix: valueSuffix
|
suffix: valueSuffix
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -472,6 +553,7 @@ final class PhoneReportBarChartView: UIView {
|
||||||
private var barSpacing: CGFloat = 0
|
private var barSpacing: CGFloat = 0
|
||||||
private var baselineY: CGFloat = 0
|
private var baselineY: CGFloat = 0
|
||||||
private let tipLab = UILabel()
|
private let tipLab = UILabel()
|
||||||
|
private let tipArrowView = UIImageView()
|
||||||
private let baseline = UIView()
|
private let baseline = UIView()
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
|
|
@ -488,6 +570,10 @@ final class PhoneReportBarChartView: UIView {
|
||||||
tipLab.isHidden = true
|
tipLab.isHidden = true
|
||||||
tipLab.isUserInteractionEnabled = false
|
tipLab.isUserInteractionEnabled = false
|
||||||
addSubview(tipLab)
|
addSubview(tipLab)
|
||||||
|
tipArrowView.contentMode = .scaleAspectFit
|
||||||
|
tipArrowView.isHidden = true
|
||||||
|
tipArrowView.isUserInteractionEnabled = false
|
||||||
|
addSubview(tipArrowView)
|
||||||
|
|
||||||
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
|
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
|
||||||
addGestureRecognizer(tap)
|
addGestureRecognizer(tap)
|
||||||
|
|
@ -500,6 +586,7 @@ final class PhoneReportBarChartView: UIView {
|
||||||
barGradient: (UIColor, UIColor),
|
barGradient: (UIColor, UIColor),
|
||||||
highlightGradient: (UIColor, UIColor),
|
highlightGradient: (UIColor, UIColor),
|
||||||
tipColor: UIColor,
|
tipColor: UIColor,
|
||||||
|
tipArrowImageName: String,
|
||||||
suffix: String
|
suffix: String
|
||||||
) {
|
) {
|
||||||
let previous = self.bars
|
let previous = self.bars
|
||||||
|
|
@ -507,6 +594,7 @@ final class PhoneReportBarChartView: UIView {
|
||||||
self.barGradient = barGradient
|
self.barGradient = barGradient
|
||||||
self.highlightGradient = highlightGradient
|
self.highlightGradient = highlightGradient
|
||||||
self.tipColor = tipColor
|
self.tipColor = tipColor
|
||||||
|
tipArrowView.image = UIImage(named: tipArrowImageName)
|
||||||
self.suffix = suffix
|
self.suffix = suffix
|
||||||
let dataChanged = previous.count != bars.count
|
let dataChanged = previous.count != bars.count
|
||||||
|| zip(previous, bars).contains { $0.label != $1.label || $0.value != $1.value }
|
|| zip(previous, bars).contains { $0.label != $1.label || $0.value != $1.value }
|
||||||
|
|
@ -540,6 +628,7 @@ final class PhoneReportBarChartView: UIView {
|
||||||
labelViews.removeAll()
|
labelViews.removeAll()
|
||||||
barFrames.removeAll()
|
barFrames.removeAll()
|
||||||
tipLab.isHidden = true
|
tipLab.isHidden = true
|
||||||
|
tipArrowView.isHidden = true
|
||||||
guard !bars.isEmpty else {
|
guard !bars.isEmpty else {
|
||||||
baseline.isHidden = true
|
baseline.isHidden = true
|
||||||
return
|
return
|
||||||
|
|
@ -549,7 +638,11 @@ final class PhoneReportBarChartView: UIView {
|
||||||
let maxVal = max(bars.map(\.value).max() ?? 1, 1)
|
let maxVal = max(bars.map(\.value).max() ?? 1, 1)
|
||||||
let count = bars.count
|
let count = bars.count
|
||||||
let labelH: CGFloat = 16
|
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
|
let barW: CGFloat = 16
|
||||||
barSpacing = count > 1
|
barSpacing = count > 1
|
||||||
? max(0, (bounds.width - barW * CGFloat(count)) / CGFloat(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.startPoint = CGPoint(x: 0.5, y: 0)
|
||||||
gradient.endPoint = CGPoint(x: 0.5, y: 1)
|
gradient.endPoint = CGPoint(x: 0.5, y: 1)
|
||||||
gradient.cornerRadius = 4
|
gradient.cornerRadius = 4
|
||||||
|
gradient.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||||
layer.insertSublayer(gradient, below: tipLab.layer)
|
layer.insertSublayer(gradient, below: tipLab.layer)
|
||||||
barLayers.append(gradient)
|
barLayers.append(gradient)
|
||||||
|
|
||||||
|
|
@ -594,18 +688,25 @@ final class PhoneReportBarChartView: UIView {
|
||||||
tipLab.backgroundColor = tipColor
|
tipLab.backgroundColor = tipColor
|
||||||
tipLab.sizeToFit()
|
tipLab.sizeToFit()
|
||||||
let tipW = tipLab.bounds.width + 5 + 5
|
let tipW = tipLab.bounds.width + 5 + 5
|
||||||
let tipH = tipLab.bounds.height + 4 + 4
|
|
||||||
// 与柱子水平居中,允许超出 chart 左右边界
|
// 与柱子水平居中,允许超出 chart 左右边界
|
||||||
tipLab.frame = CGRect(
|
tipLab.frame = CGRect(
|
||||||
x: x + barW / 2 - tipW / 2,
|
x: x + barW / 2 - tipW / 2,
|
||||||
y: max(0, y - tipH - 4),
|
y: max(0, y - tipHeight - tipArrowHeight - tipArrowBarSpacing),
|
||||||
width: tipW,
|
width: tipW,
|
||||||
height: tipH
|
height: tipHeight
|
||||||
)
|
)
|
||||||
tipLab.isHidden = false
|
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(tipLab)
|
||||||
|
bringSubviewToFront(tipArrowView)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func tipText(for bar: PhoneReportDetailViewModel.ChartBar) -> String {
|
private func tipText(for bar: PhoneReportDetailViewModel.ChartBar) -> String {
|
||||||
|
|
@ -620,3 +721,27 @@ final class PhoneReportBarChartView: UIView {
|
||||||
return "\(mins)分钟"
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,10 @@ final class PhoneReportDetailViewModel {
|
||||||
batteryText: local.batteryPercent == nil ? placeholder : local.batteryText,
|
batteryText: local.batteryPercent == nil ? placeholder : local.batteryText,
|
||||||
batteryPercent: local.batteryPercent,
|
batteryPercent: local.batteryPercent,
|
||||||
isCharging: local.isCharging,
|
isCharging: local.isCharging,
|
||||||
estimateText: placeholder,
|
estimateText: Self.estimatedUsageText(
|
||||||
|
batteryPercent: local.batteryPercent,
|
||||||
|
placeholder: placeholder
|
||||||
|
),
|
||||||
brightnessText: local.brightnessText,
|
brightnessText: local.brightnessText,
|
||||||
volumeText: local.volumeText,
|
volumeText: local.volumeText,
|
||||||
wifiText: local.network,
|
wifiText: local.network,
|
||||||
|
|
@ -162,11 +165,15 @@ final class PhoneReportDetailViewModel {
|
||||||
private func makeRemoteSnapshot(report: PhoneUsageReportModel) -> StatusSnapshot {
|
private func makeRemoteSnapshot(report: PhoneUsageReportModel) -> StatusSnapshot {
|
||||||
let info = report.phoneInfo
|
let info = report.phoneInfo
|
||||||
let charts = chartFields(from: report)
|
let charts = chartFields(from: report)
|
||||||
|
let batteryPercent = Self.percent(from: info?.battery)
|
||||||
return StatusSnapshot(
|
return StatusSnapshot(
|
||||||
batteryText: Self.displayText(info?.battery, placeholder: placeholder),
|
batteryText: Self.displayText(info?.battery, placeholder: placeholder),
|
||||||
batteryPercent: Self.percent(from: info?.battery),
|
batteryPercent: batteryPercent,
|
||||||
isCharging: false,
|
isCharging: false,
|
||||||
estimateText: placeholder,
|
estimateText: Self.estimatedUsageText(
|
||||||
|
batteryPercent: batteryPercent,
|
||||||
|
placeholder: placeholder
|
||||||
|
),
|
||||||
brightnessText: Self.displayText(info?.brightness, placeholder: placeholder),
|
brightnessText: Self.displayText(info?.brightness, placeholder: placeholder),
|
||||||
volumeText: Self.displayText(info?.volume, placeholder: placeholder),
|
volumeText: Self.displayText(info?.volume, placeholder: placeholder),
|
||||||
wifiText: Self.displayText(info?.network, placeholder: placeholder),
|
wifiText: Self.displayText(info?.network, placeholder: placeholder),
|
||||||
|
|
@ -287,6 +294,13 @@ final class PhoneReportDetailViewModel {
|
||||||
return max(0, min(100, Int(number.rounded())))
|
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 {
|
private static func storageRatio(used: String?, total: String?) -> CGFloat {
|
||||||
guard let usedValue = firstNumber(in: used),
|
guard let usedValue = firstNumber(in: used),
|
||||||
let totalValue = firstNumber(in: total),
|
let totalValue = firstNumber(in: total),
|
||||||
|
|
|
||||||
|
|
@ -375,7 +375,11 @@ final class ReceiveMessagePopView: UIView {
|
||||||
avatarView.setHeadPic(item.headPic)
|
avatarView.setHeadPic(item.headPic)
|
||||||
messageImageView.kf.cancelDownloadTask()
|
messageImageView.kf.cancelDownloadTask()
|
||||||
if let imageURL = item.imageURL {
|
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 {
|
} else {
|
||||||
messageImageView.image = item.image
|
messageImageView.image = item.image
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ final class ReceiveMessageReportPopView: UIView {
|
||||||
private static let shared = ReceiveMessageReportPopView(
|
private static let shared = ReceiveMessageReportPopView(
|
||||||
frame: CGRect(origin: .zero, size: kScreenSize)
|
frame: CGRect(origin: .zero, size: kScreenSize)
|
||||||
)
|
)
|
||||||
|
private static let defaultCenterYOffset: CGFloat = 8
|
||||||
|
|
||||||
private let reasons = ["诈骗", "涉黄", "违规行为", "散播不良广告", "谩骂攻击", "其他"]
|
private let reasons = ["诈骗", "涉黄", "违规行为", "散播不良广告", "谩骂攻击", "其他"]
|
||||||
private var selectedReasons = Set<String>()
|
private var selectedReasons = Set<String>()
|
||||||
|
|
@ -52,17 +53,26 @@ final class ReceiveMessageReportPopView: UIView {
|
||||||
return view
|
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 = {
|
private let popBgView: UIView = {
|
||||||
let view = UIView()
|
let view = UIView()
|
||||||
view.backgroundColor = .white
|
view.backgroundColor = .white
|
||||||
view.layer.cornerRadius = 30
|
view.layer.cornerRadius = 40
|
||||||
view.clipsToBounds = true
|
view.clipsToBounds = true
|
||||||
return view
|
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 = {
|
private let headerBgImg: UIImageView = {
|
||||||
let view = UIImageView(image: UIImage(named: "Popup/header_bg"))
|
let view = UIImageView(image: UIImage(named: "Popup/header_bg"))
|
||||||
|
|
@ -83,9 +93,9 @@ final class ReceiveMessageReportPopView: UIView {
|
||||||
private let optionsStack: UIStackView = {
|
private let optionsStack: UIStackView = {
|
||||||
let stack = UIStackView()
|
let stack = UIStackView()
|
||||||
stack.axis = .vertical
|
stack.axis = .vertical
|
||||||
stack.spacing = 22
|
stack.spacing = 7
|
||||||
stack.alignment = .fill
|
stack.alignment = .fill
|
||||||
stack.distribution = .fillEqually
|
stack.distribution = .fill
|
||||||
return stack
|
return stack
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -175,24 +185,32 @@ final class ReceiveMessageReportPopView: UIView {
|
||||||
popView.layoutChain
|
popView.layoutChain
|
||||||
.centerX()
|
.centerX()
|
||||||
.width(315)
|
.width(315)
|
||||||
|
.height(450)
|
||||||
|
|
||||||
popCenterYConstraint = popView.centerYAnchor.constraint(equalTo: centerYAnchor)
|
popCenterYConstraint = popView.centerYAnchor.constraint(
|
||||||
|
equalTo: centerYAnchor,
|
||||||
|
constant: Self.defaultCenterYOffset
|
||||||
|
)
|
||||||
popCenterYConstraint?.isActive = true
|
popCenterYConstraint?.isActive = true
|
||||||
|
|
||||||
popBgView.layoutChain.edges()
|
popBgView.layoutChain
|
||||||
|
.top(30)
|
||||||
|
.left()
|
||||||
|
.right()
|
||||||
|
.bottom()
|
||||||
|
|
||||||
logoImgView.layoutChain
|
logoImgView.layoutChain
|
||||||
.left(16)
|
.left(42)
|
||||||
.top(-45)
|
.top()
|
||||||
.width(109)
|
.width(100)
|
||||||
.height(109)
|
.height(74)
|
||||||
|
|
||||||
headerBgImg.layoutChain
|
headerBgImg.layoutChain
|
||||||
.edges(excludingEdge: .bottom)
|
.edges(excludingEdge: .bottom)
|
||||||
.heightToWidth(100 / 315)
|
.heightToWidth(100 / 315)
|
||||||
|
|
||||||
titleLabel.layoutChain
|
titleLabel.layoutChain
|
||||||
.top(70)
|
.top(48)
|
||||||
.edgesHorzontal(20)
|
.edgesHorzontal(20)
|
||||||
|
|
||||||
optionViews = reasons.map { reason in
|
optionViews = reasons.map { reason in
|
||||||
|
|
@ -202,30 +220,31 @@ final class ReceiveMessageReportPopView: UIView {
|
||||||
let rowStack = UIStackView()
|
let rowStack = UIStackView()
|
||||||
rowStack.axis = .horizontal
|
rowStack.axis = .horizontal
|
||||||
rowStack.alignment = .fill
|
rowStack.alignment = .fill
|
||||||
rowStack.distribution = .fillEqually
|
rowStack.distribution = .fill
|
||||||
rowStack.spacing = 8
|
rowStack.spacing = 0
|
||||||
rowStack.isUserInteractionEnabled = true
|
rowStack.isUserInteractionEnabled = true
|
||||||
rowStack.addArrangedSubview(optionViews[row * 2])
|
rowStack.addArrangedSubview(optionViews[row * 2])
|
||||||
rowStack.addArrangedSubview(optionViews[row * 2 + 1])
|
rowStack.addArrangedSubview(optionViews[row * 2 + 1])
|
||||||
|
optionViews[row * 2].layoutChain.width(118)
|
||||||
optionsStack.addArrangedSubview(rowStack)
|
optionsStack.addArrangedSubview(rowStack)
|
||||||
rowStack.layoutChain.height(36)
|
rowStack.layoutChain.height(36)
|
||||||
}
|
}
|
||||||
optionsStack.isUserInteractionEnabled = true
|
optionsStack.isUserInteractionEnabled = true
|
||||||
|
|
||||||
optionsStack.layoutChain
|
optionsStack.layoutChain
|
||||||
.topToBottomOfView(titleLabel, offset: 24)
|
.topToBottomOfView(titleLabel, offset: 18)
|
||||||
.left(24)
|
.left(32)
|
||||||
.right(20)
|
.right(18)
|
||||||
popBgView.bringSubviewToFront(optionsStack)
|
popBgView.bringSubviewToFront(optionsStack)
|
||||||
popBgView.bringSubviewToFront(inputContainer)
|
popBgView.bringSubviewToFront(inputContainer)
|
||||||
popBgView.bringSubviewToFront(confirmBtn)
|
popBgView.bringSubviewToFront(confirmBtn)
|
||||||
popBgView.bringSubviewToFront(cancelBtn)
|
popBgView.bringSubviewToFront(cancelBtn)
|
||||||
|
|
||||||
inputContainer.layoutChain
|
inputContainer.layoutChain
|
||||||
.topToBottomOfView(optionsStack, offset: 16)
|
.topToBottomOfView(optionsStack, offset: 10)
|
||||||
.left(20)
|
.left(30)
|
||||||
.right(20)
|
.right(24)
|
||||||
.height(86)
|
.height(80)
|
||||||
|
|
||||||
remarkTextView.layoutChain.edges()
|
remarkTextView.layoutChain.edges()
|
||||||
placeholderLabel.layoutChain
|
placeholderLabel.layoutChain
|
||||||
|
|
@ -244,7 +263,7 @@ final class ReceiveMessageReportPopView: UIView {
|
||||||
.left(20)
|
.left(20)
|
||||||
.right(20)
|
.right(20)
|
||||||
.height(50)
|
.height(50)
|
||||||
.bottom(8)
|
.bottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupActions() {
|
private func setupActions() {
|
||||||
|
|
@ -266,7 +285,7 @@ final class ReceiveMessageReportPopView: UIView {
|
||||||
remarkTextView.text = ""
|
remarkTextView.text = ""
|
||||||
placeholderLabel.isHidden = false
|
placeholderLabel.isHidden = false
|
||||||
optionViews.forEach { $0.isChosen = false }
|
optionViews.forEach { $0.isChosen = false }
|
||||||
popCenterYConstraint?.constant = 0
|
popCenterYConstraint?.constant = Self.defaultCenterYOffset
|
||||||
updateConfirmState()
|
updateConfirmState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -342,7 +361,9 @@ final class ReceiveMessageReportPopView: UIView {
|
||||||
let keyboardFrame = convert(frame, from: nil)
|
let keyboardFrame = convert(frame, from: nil)
|
||||||
let inputFrame = inputContainer.convert(inputContainer.bounds, to: self)
|
let inputFrame = inputContainer.convert(inputContainer.bounds, to: self)
|
||||||
let overlap = inputFrame.maxY + 16 - keyboardFrame.minY
|
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) {
|
UIView.animate(withDuration: duration) {
|
||||||
self.layoutIfNeeded()
|
self.layoutIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
@ -350,7 +371,7 @@ final class ReceiveMessageReportPopView: UIView {
|
||||||
|
|
||||||
@objc private func keyboardWillHide(_ notification: Notification) {
|
@objc private func keyboardWillHide(_ notification: Notification) {
|
||||||
let duration = (notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double) ?? 0.25
|
let duration = (notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double) ?? 0.25
|
||||||
popCenterYConstraint?.constant = 0
|
popCenterYConstraint?.constant = Self.defaultCenterYOffset
|
||||||
UIView.animate(withDuration: duration) {
|
UIView.animate(withDuration: duration) {
|
||||||
self.layoutIfNeeded()
|
self.layoutIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
@ -432,8 +453,8 @@ final class ReceiveMessageReportOptionView: UIButton {
|
||||||
boxView.layoutChain
|
boxView.layoutChain
|
||||||
.left()
|
.left()
|
||||||
.centerY()
|
.centerY()
|
||||||
.width(22)
|
.width(20)
|
||||||
.height(22)
|
.height(20)
|
||||||
|
|
||||||
reasonLabel.layoutChain
|
reasonLabel.layoutChain
|
||||||
.leftToRightOfView(boxView, offset: 8)
|
.leftToRightOfView(boxView, offset: 8)
|
||||||
|
|
@ -455,13 +476,13 @@ final class ReceiveMessageReportOptionView: UIButton {
|
||||||
fatalError("init(coder:) has not been implemented")
|
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
|
private static let normalImage = UIGraphicsImageRenderer(size: boxSize).image { _ in
|
||||||
let rect = CGRect(origin: .zero, size: boxSize).insetBy(dx: 1, dy: 1)
|
let rect = CGRect(origin: .zero, size: boxSize).insetBy(dx: 1, dy: 1)
|
||||||
UIColor.white.setFill()
|
UIColor.white.setFill()
|
||||||
UIBezierPath(ovalIn: rect).fill()
|
UIBezierPath(ovalIn: rect).fill()
|
||||||
UIColor(hexStr: "#D0D5DD").setStroke()
|
UIColor(hexStr: "#C5C7CA").setStroke()
|
||||||
let border = UIBezierPath(ovalIn: rect)
|
let border = UIBezierPath(ovalIn: rect)
|
||||||
border.lineWidth = 1.5
|
border.lineWidth = 1.5
|
||||||
border.stroke()
|
border.stroke()
|
||||||
|
|
@ -476,9 +497,9 @@ final class ReceiveMessageReportOptionView: UIButton {
|
||||||
check.lineWidth = 1.8
|
check.lineWidth = 1.8
|
||||||
check.lineCapStyle = .round
|
check.lineCapStyle = .round
|
||||||
check.lineJoinStyle = .round
|
check.lineJoinStyle = .round
|
||||||
check.move(to: CGPoint(x: 6.2, y: 11.4))
|
check.move(to: CGPoint(x: 5.5, y: 10.4))
|
||||||
check.addLine(to: CGPoint(x: 9.4, y: 14.6))
|
check.addLine(to: CGPoint(x: 8.6, y: 13.5))
|
||||||
check.addLine(to: CGPoint(x: 16.0, y: 7.8))
|
check.addLine(to: CGPoint(x: 14.8, y: 7.2))
|
||||||
UIColor.white.setStroke()
|
UIColor.white.setStroke()
|
||||||
check.stroke()
|
check.stroke()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,38 @@ final class SOSReceiveAlertView: UIView {
|
||||||
setNeedsLayout()
|
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() {
|
func dismiss() {
|
||||||
stopAnimations()
|
stopAnimations()
|
||||||
secondaryAvatarViews.values.forEach { $0.removeFromSuperview() }
|
secondaryAvatarViews.values.forEach { $0.removeFromSuperview() }
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ class SearchLocationVC: BaseViewController {
|
||||||
DLToast.show(text: "请输入正确的手机号码")
|
DLToast.show(text: "请输入正确的手机号码")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
self.rootView.phoneInputTF.resignFirstResponder()
|
||||||
self.requestSearchPhone(phone: phone)
|
self.requestSearchPhone(phone: phone)
|
||||||
self.rootView.searchProgressView.isHidden = false
|
self.rootView.searchProgressView.isHidden = false
|
||||||
self.rootView.playVideo {
|
self.rootView.playVideo {
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,11 @@ final class LockDistractVC: BaseViewController {
|
||||||
if self.selectedMember?.user_id == AppContextManager.shared.userId {
|
if self.selectedMember?.user_id == AppContextManager.shared.userId {
|
||||||
AppRouter.push(Route.privacySettings)
|
AppRouter.push(Route.privacySettings)
|
||||||
} else {
|
} else {
|
||||||
DLToast.show(text: "已提醒")
|
GroupService.sendPrivilegeRemind(
|
||||||
|
content: "请开启一键锁机隐私设置",
|
||||||
|
groupKey: self.activeGroupKey,
|
||||||
|
userId: self.selectedMember?.user_id ?? ""
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,9 @@ final class LockDistractView: UIView {
|
||||||
private(set) var isCurrentUser = false
|
private(set) var isCurrentUser = false
|
||||||
private(set) var primaryAction: LockDistractPrimaryAction = .lock
|
private(set) var primaryAction: LockDistractPrimaryAction = .lock
|
||||||
private let copyMaxLength = 20
|
private let copyMaxLength = 20
|
||||||
|
private var lockBtnHeightConstraint: NSLayoutConstraint?
|
||||||
|
private var bottomStackToAppsConstraint: NSLayoutConstraint?
|
||||||
|
private var bottomStackToStatsConstraint: NSLayoutConstraint?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
@ -119,30 +122,51 @@ final class LockDistractView: UIView {
|
||||||
|
|
||||||
var onPrivilegeAction: (() -> Void)?
|
var onPrivilegeAction: (() -> Void)?
|
||||||
private(set) var isPrivilegeDenied = false
|
private(set) var isPrivilegeDenied = false
|
||||||
private let privilegeOverlay = PrivilegeDeniedOverlay()
|
private let appsPrivilegeOverlay = PrivilegeDeniedOverlay()
|
||||||
|
private let stylePrivilegeOverlay = PrivilegeDeniedOverlay()
|
||||||
|
|
||||||
func setPrivilegeDenied(_ denied: Bool, isCurrentUser: Bool) {
|
func setPrivilegeDenied(_ denied: Bool, isCurrentUser: Bool) {
|
||||||
isPrivilegeDenied = denied
|
isPrivilegeDenied = denied
|
||||||
privilegeOverlay.configure(
|
appsPrivilegeOverlay.configure(
|
||||||
|
title: "",
|
||||||
|
message: "",
|
||||||
|
actionTitle: "",
|
||||||
|
imageName: "PrivilegeDenied/lock_apps"
|
||||||
|
)
|
||||||
|
stylePrivilegeOverlay.configure(
|
||||||
title: "TA未开启权限",
|
title: "TA未开启权限",
|
||||||
message: "无法查看可锁应用",
|
message: "无法查看可锁应用",
|
||||||
actionTitle: isCurrentUser ? "去开启" : "提醒TA"
|
actionTitle: isCurrentUser ? "去开启" : "提醒TA",
|
||||||
|
imageName: "PrivilegeDenied/lock_style"
|
||||||
)
|
)
|
||||||
privilegeOverlay.onAction = { [weak self] in self?.onPrivilegeAction?() }
|
stylePrivilegeOverlay.onAction = { [weak self] in self?.onPrivilegeAction?() }
|
||||||
privilegeOverlay.setVisible(denied)
|
appsPrivilegeOverlay.setVisible(denied)
|
||||||
|
stylePrivilegeOverlay.setVisible(denied)
|
||||||
if denied {
|
if denied {
|
||||||
contentView.bringSubviewToFront(privilegeOverlay)
|
appsCard.bringSubviewToFront(appsPrivilegeOverlay)
|
||||||
|
styleCard.bringSubviewToFront(stylePrivilegeOverlay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applyLockedState() {
|
private func applyLockedState() {
|
||||||
|
let lockedByOthers = primaryAction == .hidden
|
||||||
updateLockedEmptyImage()
|
updateLockedEmptyImage()
|
||||||
avatarLockView.isHidden = !isLocked
|
avatarLockView.isHidden = !isLocked
|
||||||
styleSectionTitle.isHidden = isLocked
|
styleSectionTitle.isHidden = isLocked
|
||||||
styleCard.isHidden = isLocked
|
styleCard.isHidden = isLocked
|
||||||
lockedEmptyView.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()
|
appCollectionView.reloadData()
|
||||||
lockBtn.isHidden = primaryAction == .hidden
|
lockBtn.isHidden = lockedByOthers
|
||||||
|
lockBtnHeightConstraint?.constant = lockedByOthers ? 0 : 56
|
||||||
switch primaryAction {
|
switch primaryAction {
|
||||||
case .lock:
|
case .lock:
|
||||||
tipLab.text = "锁定功能需要在对方开启权限情况下才能使用哟~"
|
tipLab.text = "锁定功能需要在对方开启权限情况下才能使用哟~"
|
||||||
|
|
@ -184,7 +208,8 @@ final class LockDistractView: UIView {
|
||||||
bottomStack.addArrangedSubview(styleSectionTitle)
|
bottomStack.addArrangedSubview(styleSectionTitle)
|
||||||
bottomStack.addArrangedSubview(styleCard)
|
bottomStack.addArrangedSubview(styleCard)
|
||||||
bottomStack.addArrangedSubview(lockedEmptyView)
|
bottomStack.addArrangedSubview(lockedEmptyView)
|
||||||
contentView.addSubview(privilegeOverlay)
|
appsCard.addSubview(appsPrivilegeOverlay)
|
||||||
|
styleCard.addSubview(stylePrivilegeOverlay)
|
||||||
|
|
||||||
navView.layoutChain
|
navView.layoutChain
|
||||||
.edges(excludingEdge: .bottom)
|
.edges(excludingEdge: .bottom)
|
||||||
|
|
@ -198,6 +223,7 @@ final class LockDistractView: UIView {
|
||||||
.bottom(kSafeBottomMargin)
|
.bottom(kSafeBottomMargin)
|
||||||
.edgesHorzontal(30)
|
.edgesHorzontal(30)
|
||||||
.height(56)
|
.height(56)
|
||||||
|
lockBtnHeightConstraint = lockBtn.jh_lastConstraint
|
||||||
|
|
||||||
tipRow.layoutChain
|
tipRow.layoutChain
|
||||||
.centerX()
|
.centerX()
|
||||||
|
|
@ -246,18 +272,23 @@ final class LockDistractView: UIView {
|
||||||
.topToBottomOfView(appsCard, offset: 11)
|
.topToBottomOfView(appsCard, offset: 11)
|
||||||
.edgesHorzontal(16)
|
.edgesHorzontal(16)
|
||||||
.bottom(20)
|
.bottom(20)
|
||||||
|
bottomStackToAppsConstraint = bottomStack.jh_constraint(
|
||||||
|
.top, toAttribute: .bottom, otherView: appsCard, relation: .equal
|
||||||
|
)
|
||||||
|
bottomStackToStatsConstraint = bottomStack.topAnchor.constraint(
|
||||||
|
equalTo: statsRow.bottomAnchor,
|
||||||
|
constant: 36
|
||||||
|
)
|
||||||
|
|
||||||
privilegeOverlay.layoutChain
|
appsPrivilegeOverlay.layoutChain.edges()
|
||||||
.topToView(appsSectionTitle)
|
appsPrivilegeOverlay.layer.cornerRadius = 26
|
||||||
.left(16)
|
stylePrivilegeOverlay.layoutChain.edges()
|
||||||
.right(16)
|
stylePrivilegeOverlay.layer.cornerRadius = 26
|
||||||
.bottomToView(bottomStack)
|
|
||||||
privilegeOverlay.layer.cornerRadius = 26
|
|
||||||
|
|
||||||
styleSectionTitle.layoutChain.left(4)
|
styleSectionTitle.layoutChain.left(4)
|
||||||
lockedEmptyView.layoutChain
|
lockedEmptyView.layoutChain
|
||||||
.centerX()
|
.centerX()
|
||||||
// .height(150)
|
.height(150)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupCopyLimit() {
|
private func setupCopyLimit() {
|
||||||
|
|
|
||||||
|
|
@ -57,10 +57,17 @@ final class MineView: UIView {
|
||||||
|
|
||||||
private lazy var headerBgView: UIView = {
|
private lazy var headerBgView: UIView = {
|
||||||
let v = UIView()
|
let v = UIView()
|
||||||
v.backgroundColor = .clear
|
v.backgroundColor = UIColor(hexStr: "#F5FBFB")
|
||||||
return v
|
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 = {
|
lazy var photoWall: MinePhotoWallView = {
|
||||||
MinePhotoWallView()
|
MinePhotoWallView()
|
||||||
}()
|
}()
|
||||||
|
|
@ -173,13 +180,14 @@ final class MineView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupUI() {
|
private func setupUI() {
|
||||||
backgroundColor = UIColor(hexStr: "#FAFAFA")
|
backgroundColor = UIColor(hexStr: "#F5FBFB")
|
||||||
|
|
||||||
addSubview(scrollView)
|
addSubview(scrollView)
|
||||||
scrollView.addSubview(contentView)
|
scrollView.addSubview(contentView)
|
||||||
contentView.addSubview(headerBgView)
|
contentView.addSubview(headerBgView)
|
||||||
|
|
||||||
// 照片墙固定顶部,不随列表滚动
|
// 照片墙固定顶部,不随列表滚动
|
||||||
|
addSubview(navBgView)
|
||||||
addSubview(photoWall)
|
addSubview(photoWall)
|
||||||
|
|
||||||
contentView.addSubview(vipCardView)
|
contentView.addSubview(vipCardView)
|
||||||
|
|
@ -197,6 +205,18 @@ final class MineView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupLayout() {
|
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
|
photoWall.layoutChain
|
||||||
.top(40)
|
.top(40)
|
||||||
.edgesHorzontal()
|
.edgesHorzontal()
|
||||||
|
|
@ -310,14 +330,6 @@ final class MineView: UIView {
|
||||||
|
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews() {
|
||||||
super.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()
|
refreshTableViewHeight()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -99,10 +99,9 @@ final class MoodStatusVC: BaseViewController {
|
||||||
.subscribe(onNext: { [weak self] _ in
|
.subscribe(onNext: { [weak self] _ in
|
||||||
self?.viewModel.endSaving()
|
self?.viewModel.endSaving()
|
||||||
AppContextManager.shared.updateAccountMood(moodIndex)
|
AppContextManager.shared.updateAccountMood(moodIndex)
|
||||||
DLToast.show(text: "保存成功") {
|
self?.rootView.setSaving(false)
|
||||||
AppRouter.shared.popOrDismiss()
|
DLToast.show(text: "保存成功")
|
||||||
NotificationCenter.default.post(name: .RefreshGroupInfoNotification, object: nil)
|
NotificationCenter.default.post(name: .RefreshGroupInfoNotification, object: nil)
|
||||||
}
|
|
||||||
}, onError: { [weak self] _ in
|
}, onError: { [weak self] _ in
|
||||||
self?.viewModel.endSaving()
|
self?.viewModel.endSaving()
|
||||||
self?.rootView.setSaving(false)
|
self?.rootView.setSaving(false)
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ class AvatarIconListVC: BaseViewController {
|
||||||
let selected = self.rootView.selectedHeadPic
|
let selected = self.rootView.selectedHeadPic
|
||||||
guard !selected.isEmpty else { return }
|
guard !selected.isEmpty else { return }
|
||||||
self.onSelectIcon?(selected)
|
self.onSelectIcon?(selected)
|
||||||
AppRouter.shared.popOrDismiss()
|
|
||||||
}).disposed(by: disposeBag)
|
}).disposed(by: disposeBag)
|
||||||
|
|
||||||
rootView.onBadgeTap = { [weak self] in
|
rootView.onBadgeTap = { [weak self] in
|
||||||
|
|
@ -84,7 +83,6 @@ class AvatarIconListVC: BaseViewController {
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
self.rootView.setPreviewImage(image)
|
self.rootView.setPreviewImage(image)
|
||||||
self.onSelectIcon?(url)
|
self.onSelectIcon?(url)
|
||||||
AppRouter.shared.popOrDismiss()
|
|
||||||
}, onError: { error in
|
}, onError: { error in
|
||||||
DLToast.show(text: error.gatewayMessage ?? "更换失败")
|
DLToast.show(text: error.gatewayMessage ?? "更换失败")
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,11 @@ final class PigeonHistoryCell: UITableViewCell {
|
||||||
if let image = item.image, !isVoice {
|
if let image = item.image, !isVoice {
|
||||||
photoView.image = image
|
photoView.image = image
|
||||||
} else if let mediaURL = item.mediaURL, !isVoice {
|
} 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 {
|
} else {
|
||||||
photoView.image = nil
|
photoView.image = nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -679,7 +679,7 @@ private extension PigeonMessageView {
|
||||||
if selectedMemberIds.contains(id) {
|
if selectedMemberIds.contains(id) {
|
||||||
selectedMemberIds.remove(id)
|
selectedMemberIds.remove(id)
|
||||||
} else if !member.extra.privilege.pigeon_mail {
|
} else if !member.extra.privilege.pigeon_mail {
|
||||||
DLToast.show(text: "该用户未开启设置,无法接收飞鸽传书")
|
DLToast.show(text: "TA没有开启隐私设置\n无法使用飞鸽传书")
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
selectedMemberIds.insert(id)
|
selectedMemberIds.insert(id)
|
||||||
|
|
|
||||||
|
|
@ -235,4 +235,40 @@ struct GroupService {
|
||||||
.do(onSuccess: notifyGroupInfoChange(operation: "dismiss", requestData: requestData))
|
.do(onSuccess: notifyGroupInfoChange(operation: "dismiss", requestData: requestData))
|
||||||
.asObservable()
|
.asObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 提醒对方开启权限
|
||||||
|
static func remindPrivilege(
|
||||||
|
content: String,
|
||||||
|
groupKey: String,
|
||||||
|
userId: String,
|
||||||
|
title: String = "请开启权限"
|
||||||
|
) -> Observable<ResponseModel> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -331,8 +331,6 @@ public class DLToast {
|
||||||
if let toastView = superView.findSubView(with: DLToastConfig.tag) as? DLToastView {
|
if let toastView = superView.findSubView(with: DLToastConfig.tag) as? DLToastView {
|
||||||
toastView.removeFromSuperview()
|
toastView.removeFromSuperview()
|
||||||
}
|
}
|
||||||
let config = config
|
|
||||||
config.containerRadius = icon == nil ? 4 : config.containerRadius
|
|
||||||
let toastView = DLToastView(iconImage: icon, text: text, config: config, style: style)
|
let toastView = DLToastView(iconImage: icon, text: text, config: config, style: style)
|
||||||
toastView.tag = DLToastConfig.tag
|
toastView.tag = DLToastConfig.tag
|
||||||
toastView.completion = completion
|
toastView.completion = completion
|
||||||
|
|
|
||||||