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