// // PrivacySettingsViewController.swift // QuickLocation // import UIKit import RxSwift import Kingfisher enum PrivacySettingItem: CaseIterable { case phoneReport case groupLock case pigeonMessage var title: String { switch self { case .phoneReport: return "手机报告" case .groupLock: return "一键锁机" case .pigeonMessage: return "飞鸽传书" } } var imageName: String { switch self { case .phoneReport: return "PrivacySettings/phone_report" case .groupLock: return "PrivacySettings/group_lock" case .pigeonMessage: return "PrivacySettings/pigeon_message" } } var apiKey: String { switch self { case .phoneReport: return "phone_report" case .groupLock: return "app_lock" case .pigeonMessage: return "pigeon_mail" } } } final class PrivacySettingsViewController: BaseViewController { private var rootView: PrivacySettingsView! private var groups: [GroupInfoModel] = [] private var isSubmitting = false override func loadView() { rootView = PrivacySettingsView(frame: UIScreen.main.bounds) view = rootView } override func viewDidLoad() { super.viewDidLoad() bind() requestGroups() } private func bind() { rootView.onGroupChanged = { [weak self] in guard let self else { return } self.loadPrivilege(for: self.rootView.selectedGroupKey) } rootView.onToggle = { [weak self] item, isOn in self?.changePrivilege(item: item, isOn: isOn) } } private func requestGroups() { rootView.setSettingsEnabled(false) GroupService.groupInfo() .subscribe(onNext: { [weak self] response in guard let self else { return } guard let model = response.model else { self.groups = [] self.rootView.configure(groups: [], selectedGroupKey: "") DLToast.showError(text: response.message ?? "获取圈子信息失败") return } self.groups = model.groups self.rootView.configure( groups: model.groups, selectedGroupKey: model.default_group_key ) self.loadPrivilege(for: self.rootView.selectedGroupKey) }, onError: { [weak self] error in self?.groups = [] self?.rootView.configure(groups: [], selectedGroupKey: "") DLToast.showError(error) }) .disposed(by: disposeBag) } private func applySelectedPrivilege() { let privilege = groups.first(where: { $0.group_key == rootView.selectedGroupKey })?.privilege ?? GroupPrivilege() rootView.apply(privilege) } private func loadPrivilege(for groupKey: String) { guard !groupKey.isEmpty else { applySelectedPrivilege() return } rootView.setSettingsEnabled(false) GroupService.groupUsers(groupKey: groupKey) .subscribe(onNext: { [weak self] response in guard let self else { return } self.rootView.setSettingsEnabled(!self.groups.isEmpty) guard response.isValid(for: groupKey), self.rootView.selectedGroupKey == groupKey else { self.applySelectedPrivilege() return } let mine = response.list.first { $0.user_id == AppContextManager.shared.userId } if let index = self.groups.firstIndex(where: { $0.group_key == groupKey }) { self.groups[index].privilege = mine?.extra.privilege ?? GroupPrivilege() } self.applySelectedPrivilege() }, onError: { [weak self] error in self?.rootView.setSettingsEnabled(!(self?.groups.isEmpty ?? true)) self?.applySelectedPrivilege() DLToast.showError(error) }) .disposed(by: disposeBag) } private func changePrivilege(item: PrivacySettingItem, isOn: Bool) { let groupKey = rootView.selectedGroupKey guard !groupKey.isEmpty, let index = groups.firstIndex(where: { $0.group_key == groupKey }) else { applySelectedPrivilege() return } guard !isSubmitting else { applySelectedPrivilege() return } if item == .groupLock, !isOn { interceptTurnOffAppLock { [weak self] blocked in guard let self else { return } if blocked { self.applySelectedPrivilege() return } self.submitPrivilege(item: item, isOn: isOn, groupKey: groupKey, index: index) } return } submitPrivilege(item: item, isOn: isOn, groupKey: groupKey, index: index) } private func interceptTurnOffAppLock(completion: @escaping (Bool) -> Void) { if let record = lockRecordForPopup(from: PhoneLockSession.currentLocks) { LockedAppPopView.show(record) completion(true) return } let userId = AppContextManager.shared.userId.trimmed let revision = AppUnlockCoordinator.shared.currentLockStateRevision() isSubmitting = true rootView.setSettingsEnabled(false) UserService.phoneLocked(os: "ios") .observe(on: MainScheduler.instance) .subscribe(onNext: { [weak self] response in guard let self else { return } self.isSubmitting = false self.rootView.setSettingsEnabled(!self.groups.isEmpty) let locks = AppUnlockCoordinator.shared.locksForRestoration( response.model?.locks ?? [], startedAt: revision, userId: userId ) ?? [] if !locks.isEmpty { PhoneLockSession.currentLocks = locks } if let record = self.lockRecordForPopup(from: locks) { LockedAppPopView.show(record) completion(true) } else { completion(false) } }, onError: { [weak self] _ in guard let self else { return } self.isSubmitting = false self.rootView.setSettingsEnabled(!self.groups.isEmpty) completion(false) }) .disposed(by: disposeBag) } private func lockRecordForPopup(from locks: [PhoneLockRecord]) -> PhoneLockRecord? { let groupKey = rootView.selectedGroupKey return locks.first { $0.groupKey == groupKey } ?? locks.first } private func submitPrivilege(item: PrivacySettingItem, isOn: Bool, groupKey: String, index: Int) { let previous = groups[index].privilege var updated = previous switch item { case .phoneReport: updated.phone_report = isOn case .groupLock: updated.app_lock = isOn case .pigeonMessage: updated.pigeon_mail = isOn } groups[index].privilege = updated rootView.apply(updated) isSubmitting = true rootView.setSettingsEnabled(false) GroupService.operate( opType: "changeprivilege", requestData: [ "group_key": groupKey, "privilege_code": item.apiKey, "enable": isOn ] ) .subscribe(onNext: { [weak self] response in guard let self else { return } self.isSubmitting = false self.rootView.setSettingsEnabled(!self.groups.isEmpty) guard response.code == "0" else { self.revertPrivilege(groupKey: groupKey, previous: previous) DLToast.showError(text: response.message ?? "设置失败") return } }, onError: { [weak self] error in guard let self else { return } self.isSubmitting = false self.rootView.setSettingsEnabled(!self.groups.isEmpty) self.revertPrivilege(groupKey: groupKey, previous: previous) DLToast.showError(error) }) .disposed(by: disposeBag) } private func revertPrivilege(groupKey: String, previous: GroupPrivilege) { if let index = groups.firstIndex(where: { $0.group_key == groupKey }) { groups[index].privilege = previous } applySelectedPrivilege() } } final class PrivacySettingsView: UIView { var onGroupChanged: (() -> Void)? var onToggle: ((PrivacySettingItem, Bool) -> Void)? var selectedGroupKey: String { groupSelectorView.selectedGroupKey } private let navBackgroundView = UIImageView(image: UIImage(named: "Common/navBar_bg_2")) private let navView = BaseNavigationView(title: "隐私设置") private let scrollView = UIScrollView() private let contentView = UIView() private let groupSelectorView = PrivacyGroupSelectorView() private let phoneReportRow = PrivacySettingRow(item: .phoneReport) private let groupLockRow = PrivacySettingRow(item: .groupLock) private let pigeonMessageRow = PrivacySettingRow(item: .pigeonMessage) private let footerLabel = UILabel() override init(frame: CGRect) { super.init(frame: frame) setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(groups: [GroupInfoModel], selectedGroupKey: String) { groupSelectorView.configure(groups: groups, selectedGroupKey: selectedGroupKey) setSettingsEnabled(!groups.isEmpty) } func apply(_ privilege: GroupPrivilege) { phoneReportRow.setOn(privilege.phone_report) groupLockRow.setOn(privilege.app_lock) pigeonMessageRow.setOn(privilege.pigeon_mail) } func setSettingsEnabled(_ enabled: Bool) { [phoneReportRow, groupLockRow, pigeonMessageRow].forEach { $0.setSwitchEnabled(enabled) } } private func setupUI() { backgroundColor = UIColor(hexStr: "#FAFAFA") navBackgroundView.contentMode = .scaleAspectFill navBackgroundView.clipsToBounds = true addSubview(navBackgroundView) addSubview(navView) scrollView.showsVerticalScrollIndicator = false scrollView.alwaysBounceVertical = false addSubview(scrollView) scrollView.addSubview(contentView) footerLabel.text = "ⓘ 仅对当前选中圈子生效哟~" footerLabel.textColor = UIColor(hexStr: "#536177") footerLabel.font = .systemFont(ofSize: 13, weight: .regular) footerLabel.textAlignment = .center addSubview(footerLabel) contentView.addSubview(groupSelectorView) let shareHeader = makeSectionHeader(title: "圈子中可以共享的数据") contentView.addSubview(shareHeader) contentView.addSubview(phoneReportRow) let ownerHeader = makeSectionHeader(title: "圈主可以对你使用的功能") contentView.addSubview(ownerHeader) contentView.addSubview(groupLockRow) contentView.addSubview(pigeonMessageRow) navBackgroundView.layoutChain .edges(excludingEdge: .bottom) .height(kNaviHeight) navView.layoutChain .edges(excludingEdge: .bottom) .height(kNaviHeight) footerLabel.layoutChain .left(20) .right(20) .bottom(kSafeBottomMargin + 12) .height(24) scrollView.layoutChain .topToBottomOfView(navView) .left() .right() .bottomToTopOfView(footerLabel, offset: -8) contentView.layoutChain .edges() .widthToView(scrollView) groupSelectorView.layoutChain .top(12) .left(15) .right(15) .height(100) shareHeader.layoutChain .topToBottomOfView(groupSelectorView, offset: 20) .left(17) .right(15) .height(24) phoneReportRow.layoutChain .topToBottomOfView(shareHeader, offset: 12) .left(15) .right(15) .height(64) ownerHeader.layoutChain .topToBottomOfView(phoneReportRow, offset: 18) .left(17) .right(15) .height(24) groupLockRow.layoutChain .topToBottomOfView(ownerHeader, offset: 12) .left(15) .right(15) .height(64) pigeonMessageRow.layoutChain .topToBottomOfView(groupLockRow, offset: 12) .left(15) .right(15) .height(64) .bottom(20) groupSelectorView.onSelectionChanged = { [weak self] in self?.onGroupChanged?() } phoneReportRow.onToggle = { [weak self] isOn in self?.onToggle?(.phoneReport, isOn) } groupLockRow.onToggle = { [weak self] isOn in self?.onToggle?(.groupLock, isOn) } pigeonMessageRow.onToggle = { [weak self] isOn in self?.onToggle?(.pigeonMessage, isOn) } } private func makeSectionHeader(title: String) -> UIView { let view = UIView() let sparkleLabel = UILabel() sparkleLabel.text = "✦" sparkleLabel.textColor = UIColor(hexStr: "#08ADEF") sparkleLabel.font = .systemFont(ofSize: 18, weight: .bold) sparkleLabel.textAlignment = .center view.addSubview(sparkleLabel) let titleLabel = UILabel() titleLabel.text = title titleLabel.textColor = UIColor(hexStr: "#28364B") titleLabel.font = .systemFont(ofSize: 16, weight: .regular) view.addSubview(titleLabel) sparkleLabel.layoutChain .left() .centerY() .width(18) .height(24) titleLabel.layoutChain .leftToRightOfView(sparkleLabel, offset: 3) .right() .centerY() .height(24) return view } } private final class PrivacyGroupSelectorView: UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { var onSelectionChanged: (() -> Void)? private(set) var selectedGroupKey = "" private let layout = UICollectionViewFlowLayout() private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) private let emptyLabel = UILabel() private var groups: [GroupInfoModel] = [] override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .white layer.cornerRadius = 20 clipsToBounds = true layout.scrollDirection = .horizontal layout.minimumLineSpacing = 4 layout.minimumInteritemSpacing = 4 layout.sectionInset = UIEdgeInsets(top: 4, left: 6, bottom: 4, right: 6) collectionView.backgroundColor = .clear collectionView.showsHorizontalScrollIndicator = false collectionView.alwaysBounceHorizontal = true collectionView.dataSource = self collectionView.delegate = self collectionView.register(PrivacyGroupCell.self) addSubview(collectionView) collectionView.layoutChain.edges() emptyLabel.text = "暂无圈子" emptyLabel.textColor = UIColor(hexStr: "#9BA3AF") emptyLabel.font = .systemFont(ofSize: 15, weight: .regular) emptyLabel.textAlignment = .center emptyLabel.isHidden = true addSubview(emptyLabel) emptyLabel.layoutChain.edges() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(groups: [GroupInfoModel], selectedGroupKey: String) { self.groups = groups if groups.contains(where: { $0.group_key == selectedGroupKey }) { self.selectedGroupKey = selectedGroupKey } else { self.selectedGroupKey = groups.first?.group_key ?? "" } emptyLabel.isHidden = !groups.isEmpty collectionView.isHidden = groups.isEmpty collectionView.reloadData() guard let index = groups.firstIndex(where: { $0.group_key == self.selectedGroupKey }) else { return } collectionView.layoutIfNeeded() collectionView.scrollToItem( at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: false ) } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { groups.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell: PrivacyGroupCell = collectionView.dequeueReusableCell(for: indexPath) let group = groups[indexPath.item] cell.configure(group: group, selected: group.group_key == selectedGroupKey) return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let groupKey = groups[indexPath.item].group_key guard selectedGroupKey != groupKey else { return } selectedGroupKey = groupKey collectionView.reloadData() collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) onSelectionChanged?() } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { CGSize(width: 76, height: 92) } } private final class PrivacyGroupCell: UICollectionViewCell { private let selectionView = UIView() private let selectionPointer = UIView() private let iconView = UIImageView() private let nameLabel = UILabel() override init(frame: CGRect) { super.init(frame: frame) selectionView.backgroundColor = UIColor(hexStr: "#C9F4FF") selectionView.layer.cornerRadius = 13 selectionView.isHidden = true contentView.addSubview(selectionView) selectionPointer.backgroundColor = UIColor(hexStr: "#C9F4FF") selectionPointer.transform = CGAffineTransform(rotationAngle: .pi / 4) selectionPointer.isHidden = true contentView.addSubview(selectionPointer) iconView.contentMode = .scaleAspectFill iconView.clipsToBounds = true iconView.layer.cornerRadius = 25 contentView.addSubview(iconView) nameLabel.textColor = UIColor(hexStr: "#26364B") nameLabel.font = .systemFont(ofSize: 14, weight: .regular) nameLabel.textAlignment = .center nameLabel.lineBreakMode = .byTruncatingTail contentView.addSubview(nameLabel) selectionView.layoutChain .top(1) .left(2) .right(2) .bottom(4) selectionPointer.layoutChain .centerX() .bottom(1) .width(12) .height(12) iconView.layoutChain .top(8) .centerX() .width(50) .height(50) nameLabel.layoutChain .topToBottomOfView(iconView, offset: 5) .left(5) .right(5) .height(20) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func prepareForReuse() { super.prepareForReuse() iconView.kf.cancelDownloadTask() iconView.image = nil } func configure(group: GroupInfoModel, selected: Bool) { nameLabel.text = group.name iconView.setGroupIcon(group) selectionView.isHidden = !selected selectionPointer.isHidden = !selected } } private final class PrivacySettingRow: UIView { var onToggle: ((Bool) -> Void)? private let settingSwitch = UISwitch() init(item: PrivacySettingItem) { super.init(frame: .zero) backgroundColor = .white layer.cornerRadius = 20 let imageView = UIImageView(image: UIImage(named: item.imageName)) imageView.contentMode = .scaleAspectFit addSubview(imageView) let titleLabel = UILabel() titleLabel.text = item.title titleLabel.textColor = UIColor(hexStr: "#28364B") titleLabel.font = .systemFont(ofSize: 17, weight: .regular) addSubview(titleLabel) settingSwitch.onTintColor = UIColor(hexStr: "#08ADEF") settingSwitch.setOn(false, animated: false) settingSwitch.addTarget(self, action: #selector(switchChanged), for: .valueChanged) addSubview(settingSwitch) imageView.layoutChain .left(8) .centerY() .width(96) .height(50) settingSwitch.layoutChain .right(13) .centerY() titleLabel.layoutChain .leftToRightOfView(imageView, offset: 8) .rightToLeftOfView(settingSwitch, offset: -8) .centerY() .height(26) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setSwitchEnabled(_ enabled: Bool) { settingSwitch.isEnabled = enabled } func setOn(_ on: Bool) { settingSwitch.setOn(on, animated: false) } @objc private func switchChanged() { onToggle?(settingSwitch.isOn) } }