// // VKNimbusPanelController.swift (obfuscated from UserSettingsViewController) // VenusKitto // // Obfuscated on 2025/08/27 // import Foundation import UIKit // 外部兼容:仍然可以用旧名引用 typealias UserSettingsViewController = VKNimbusPanelController final class VKNimbusPanelController: UIViewController { // MARK: - UI (obfuscated names) private var zxAvatar: UIImage? private var zxNick: String = "未设置" private let grid: UITableView = { let t = UITableView(frame: .zero, style: .grouped) t.backgroundColor = .white t.separatorStyle = .none t.showsVerticalScrollIndicator = false t.translatesAutoresizingMaskIntoConstraints = false return t }() private let exitBtn: UIButton = { let b = UIButton(type: .system) b.setTitle("退出登录", for: .normal) b.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) b.setTitleColor(.black, for: .normal) b.backgroundColor = UIColor(hex: "#FFE059") b.layer.cornerRadius = 25 b.translatesAutoresizingMaskIntoConstraints = false return b }() private let nukeBtn: UIButton = { let b = UIButton(type: .system) b.setTitle("注销用户", for: .normal) b.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) b.setTitleColor(UIColor(hex: "#34260C"), for: .normal) b.backgroundColor = .clear b.translatesAutoresizingMaskIntoConstraints = false return b }() // 数据源(标题 + 默认值/占位资源名) private let entries: [(String, String)] = [ ("头像修改", "Home372"), ("昵称修改", "未设置") ] // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() forgeUI() pinAutoLayout() primeTableKit() bindActions() navigationItem.title = "用户设置" navigationController?.navigationBar.tintColor = .black navigationItem.leftBarButtonItem = UIBarButtonItem( image: UIImage(systemName: "chevron.left"), style: .plain, target: self, action: #selector(ax_back) ) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // 头像 if let icon = UserDefaults.standard.string(forKey: "memberIcon"), !icon.isEmpty, let url = URL(string: icon) { print("[Settings] pull avatar: \(url.absoluteString)") DispatchQueue.global().async { [weak self] in if let data = try? Data(contentsOf: url), let img = UIImage(data: data) { DispatchQueue.main.async { self?.zxAvatar = img; self?.grid.reloadData() } } } } else { zxAvatar = nil grid.reloadData() } // 昵称 zxNick = UserDefaults.standard.string(forKey: "memberName") ?? "未设置" grid.reloadData() navigationController?.setNavigationBarHidden(false, animated: animated) } // MARK: - Build UI private func forgeUI() { view.backgroundColor = .white view.addSubview(grid) view.addSubview(exitBtn) view.addSubview(nukeBtn) } private func pinAutoLayout() { NSLayoutConstraint.activate([ grid.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10), grid.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), grid.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), grid.heightAnchor.constraint(equalToConstant: 150), nukeBtn.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -10), nukeBtn.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), nukeBtn.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), nukeBtn.heightAnchor.constraint(equalToConstant: 50), exitBtn.bottomAnchor.constraint(equalTo: nukeBtn.topAnchor, constant: -20), exitBtn.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), exitBtn.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), exitBtn.heightAnchor.constraint(equalToConstant: 50) ]) } private func primeTableKit() { grid.register(VXSettingCell.self, forCellReuseIdentifier: VXSettingCell.reuseId) grid.dataSource = self grid.delegate = self grid.isScrollEnabled = false grid.layer.cornerRadius = 8 } private func bindActions() { exitBtn.addTarget(self, action: #selector(ax_logout), for: .touchUpInside) nukeBtn.addTarget(self, action: #selector(ax_deleteAccount), for: .touchUpInside) } // MARK: - Navigation @objc private func ax_back() { if presentingViewController != nil { dismiss(animated: true) } else { navigationController?.popViewController(animated: true) } } // MARK: - Actions @objc private func ax_logout() { askLogout() } @objc private func ax_deleteAccount() { askDestruct() } private func askLogout() { let ac = UIAlertController(title: "退出登录", message: "确定要退出当前账号吗?", preferredStyle: .alert) ac.addAction(UIAlertAction(title: "取消", style: .cancel)) ac.addAction(UIAlertAction(title: "确定", style: .destructive) { _ in self.doLogout() }) present(ac, animated: true) } private func askDestruct() { let ac = UIAlertController(title: "注销账户", message: "此操作将永久删除您的账户和所有数据,确定要继续吗?", preferredStyle: .alert) ac.addAction(UIAlertAction(title: "取消", style: .cancel)) ac.addAction(UIAlertAction(title: "注销", style: .destructive) { _ in self.doErase() }) present(ac, animated: true) } private func doLogout() { UserDefaults.standard.set(false, forKey: "isLogggedIn") UserDefaults.standard.removeObject(forKey: "userToken") vx_toast("已退出登录") DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { self.routeToLogin() } } private func doErase() { vx_toast("账户已注销") DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { self.routeToLogin() } } private func routeToLogin() { let loginVC = LoginViewController() let nav = UINavigationController(rootViewController: loginVC) guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else { return } window.rootViewController = nav UIView.transition(with: window, duration: 0.5, options: .transitionCrossDissolve, animations: nil) } } // MARK: - Table extension VKNimbusPanelController: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { entries.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: VXSettingCell.reuseId, for: indexPath) as! VXSettingCell let (title, val) = entries[indexPath.row] if indexPath.row == 0 { cell.bind(title: title, value: "", isFirst: indexPath.row == 0, isLast: indexPath.row == entries.count - 1, avatar: zxAvatar, placeholder: val) } else { cell.bind(title: title, value: zxNick, isFirst: indexPath.row == 0, isLast: indexPath.row == entries.count - 1, avatar: nil, placeholder: val) } return cell } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { indexPath.row == 0 ? 80 : 60 } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { UIView() } func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 0.1 } func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { UIView() } func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { 0.1 } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) switch indexPath.row { case 0: popAvatarPanel() case 1: presentNicknamePad() default: break } } } // MARK: - Avatar extension VKNimbusPanelController: UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIPopoverPresentationControllerDelegate { private func popAvatarPanel() { let ac = UIAlertController(title: "更换头像", message: nil, preferredStyle: .actionSheet) ac.modalPresentationStyle = .popover if let p = ac.popoverPresentationController { p.delegate = self let idx = IndexPath(row: 0, section: 0) if let cell = grid.cellForRow(at: idx) { p.sourceView = cell.contentView; p.sourceRect = cell.contentView.bounds } else { p.sourceView = view; p.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.maxY - 1, width: 1, height: 1) } p.permittedArrowDirections = [] } if UIImagePickerController.isSourceTypeAvailable(.camera) { let a = UIAlertAction(title: "拍照", style: .default) { [weak self] _ in self?.summonCamera() } a.setValue(UIColor.black, forKey: "titleTextColor"); ac.addAction(a) } let b = UIAlertAction(title: "从相册选择", style: .default) { [weak self] _ in self?.summonLibrary() } b.setValue(UIColor.black, forKey: "titleTextColor"); ac.addAction(b) let c = UIAlertAction(title: "取消", style: .cancel) c.setValue(UIColor.black, forKey: "titleTextColor"); ac.addAction(c) present(ac, animated: true) } private func summonCamera() { let p = UIImagePickerController() p.delegate = self p.sourceType = .camera p.allowsEditing = true present(p, animated: true) } private func summonLibrary() { let p = UIImagePickerController() p.delegate = self p.sourceType = .photoLibrary p.allowsEditing = true present(p, animated: true) } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { picker.dismiss(animated: true) guard let img = info[.editedImage] as? UIImage else { return } showHud() guard let data = squash(img) else { hideHud(); vx_toast("图片处理失败"); return } pushBlob(data) { [weak self] result in DispatchQueue.main.async { self?.hideHud() switch result { case .success(let url): self?.zxAvatar = img self?.grid.reloadData() self?.syncAvatarUrl(url) case .failure(let e): self?.raiseOops("上传失败: \(e.localizedDescription)") } } } } func prepareForPopoverPresentation(_ popoverPresentationController: UIPopoverPresentationController) { if popoverPresentationController.sourceView == nil && popoverPresentationController.barButtonItem == nil { popoverPresentationController.sourceView = view popoverPresentationController.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.maxY - 1, width: 1, height: 1) popoverPresentationController.permittedArrowDirections = [] } } } // MARK: - Nickname extension VKNimbusPanelController { private func presentNicknamePad() { let mask = UIView(frame: view.bounds) mask.backgroundColor = UIColor.black.withAlphaComponent(0.5) mask.alpha = 0; mask.tag = 1001 view.addSubview(mask) let panel = UIView(); panel.backgroundColor = .white; panel.layer.cornerRadius = 12; panel.translatesAutoresizingMaskIntoConstraints = false mask.addSubview(panel) let title = UILabel(); title.text = "修改昵称"; title.font = .systemFont(ofSize: 16, weight: .medium); title.textAlignment = .center; title.translatesAutoresizingMaskIntoConstraints = false let tf = UITextField(); tf.placeholder = "请输入昵称"; tf.font = .systemFont(ofSize: 16); tf.borderStyle = .roundedRect; tf.clearButtonMode = .whileEditing; tf.text = (zxNick == "未设置" ? "" : zxNick); tf.translatesAutoresizingMaskIntoConstraints = false let sep = UIView(); sep.backgroundColor = UIColor(hex: "#EEEEEE"); sep.translatesAutoresizingMaskIntoConstraints = false let cancel = UIButton(type: .system); cancel.setTitle("取消", for: .normal); cancel.setTitleColor(UIColor(hex: "#999999"), for: .normal); cancel.translatesAutoresizingMaskIntoConstraints = false let ok = UIButton(type: .system); ok.setTitle("完成", for: .normal); ok.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium); ok.setTitleColor(.black, for: .normal); ok.translatesAutoresizingMaskIntoConstraints = false panel.addSubview(title); panel.addSubview(tf); panel.addSubview(sep); panel.addSubview(cancel); panel.addSubview(ok) NSLayoutConstraint.activate([ panel.centerXAnchor.constraint(equalTo: mask.centerXAnchor), panel.centerYAnchor.constraint(equalTo: mask.centerYAnchor), panel.widthAnchor.constraint(equalToConstant: 280), panel.heightAnchor.constraint(equalToConstant: 180), title.topAnchor.constraint(equalTo: panel.topAnchor, constant: 20), title.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 20), title.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -20), tf.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 20), tf.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 20), tf.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -20), tf.heightAnchor.constraint(equalToConstant: 40), sep.topAnchor.constraint(equalTo: tf.bottomAnchor, constant: 20), sep.leadingAnchor.constraint(equalTo: panel.leadingAnchor), sep.trailingAnchor.constraint(equalTo: panel.trailingAnchor), sep.heightAnchor.constraint(equalToConstant: 0.5), cancel.topAnchor.constraint(equalTo: sep.bottomAnchor), cancel.leadingAnchor.constraint(equalTo: panel.leadingAnchor), cancel.trailingAnchor.constraint(equalTo: panel.centerXAnchor), cancel.bottomAnchor.constraint(equalTo: panel.bottomAnchor), cancel.heightAnchor.constraint(equalToConstant: 50), ok.topAnchor.constraint(equalTo: sep.bottomAnchor), ok.leadingAnchor.constraint(equalTo: panel.centerXAnchor), ok.trailingAnchor.constraint(equalTo: panel.trailingAnchor), ok.bottomAnchor.constraint(equalTo: panel.bottomAnchor), ok.heightAnchor.constraint(equalToConstant: 50) ]) cancel.addTarget(self, action: #selector(x_dismissOverlay), for: .touchUpInside) ok.addTarget(self, action: #selector(x_commitNickname), for: .touchUpInside) mask.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(x_dismissOverlay))) UIView.animate(withDuration: 0.3) { mask.alpha = 1 } tf.becomeFirstResponder() } @objc private func x_dismissOverlay() { if let mask = view.viewWithTag(1001) { UIView.animate(withDuration: 0.2, animations: { mask.alpha = 0 }) { _ in mask.removeFromSuperview() } } } @objc private func x_commitNickname() { guard let mask = view.viewWithTag(1001), let panel = mask.subviews.first, let tf = panel.subviews.first(where: { $0 is UITextField }) as? UITextField, let text = tf.text, !text.isEmpty else { vx_toast("昵称不能为空") return } rx_updateNick(text) x_dismissOverlay() } } // MARK: - Networking helpers extension VKNimbusPanelController { private func squash(_ image: UIImage) -> Data? { let target = CGSize(width: 800, height: 800) return image.x_resize(to: target)?.jpegData(compressionQuality: 0.7) } private func showHud() { let indicator = UIActivityIndicatorView(style: .large) indicator.color = .white indicator.startAnimating() let bg = UIView(frame: view.bounds) bg.backgroundColor = UIColor.black.withAlphaComponent(0.5) bg.tag = 1002 bg.addSubview(indicator) indicator.center = bg.center view.addSubview(bg) } private func hideHud() { view.viewWithTag(1002)?.removeFromSuperview() } private func pushBlob(_ imageData: Data, completion: @escaping (Result) -> Void) { guard let token = UserDefaults.standard.string(forKey: "userToken") else { completion(.failure(NSError(domain: "AuthError", code: 401, userInfo: [NSLocalizedDescriptionKey: "未找到用户凭证"]))) return } guard let url = URL(string: "\(baseURL)/common/upload") else { completion(.failure(NSError(domain: "URLError", code: 400, userInfo: [NSLocalizedDescriptionKey: "无效的服务器地址"]))) return } var req = URLRequest(url: url) req.httpMethod = "POST" req.setValue(token, forHTTPHeaderField: "Authorization") let boundary = "Boundary-\(UUID().uuidString)" req.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") var body = Data() body.append("--\(boundary)\r\n".data(using: .utf8)!) body.append("Content-Disposition: form-data; name=\"file\"; filename=\"avatar.jpg\"\r\n".data(using: .utf8)!) body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!) body.append(imageData) body.append("\r\n".data(using: .utf8)!) body.append("--\(boundary)--\r\n".data(using: .utf8)!) req.httpBody = body dbg_emitRequest(name: "上传头像(文件)", request: req, body: body) URLSession.shared.dataTask(with: req) { [weak self] data, response, error in self?.dbg_emitResponse(name: "上传头像(文件)", data: data, response: response, error: error) if let error = error { completion(.failure(error)); return } guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode), let data = data else { completion(.failure(NSError(domain: "ServerError", code: 500, userInfo: [NSLocalizedDescriptionKey: "服务器返回错误"]))) return } do { if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let codeStr: String = (json["code"] as? String) ?? ((json["code"] as? Int).map { String($0) }), codeStr == "200", let dataDict = json["data"] as? [String: Any], let imageUrl = dataDict["url"] as? String { completion(.success(imageUrl)) } else { completion(.failure(NSError(domain: "ParseError", code: 500, userInfo: [NSLocalizedDescriptionKey: "响应解析失败"]))) } } catch { completion(.failure(error)) } }.resume() } private func syncAvatarUrl(_ imageUrl: String) { guard let token = UserDefaults.standard.string(forKey: "userToken") else { raiseOops("未找到用户凭证"); return } guard let url = URL(string: "\(baseURL)/petRecordApUser/modifyMemberIcon") else { raiseOops("无效的服务器地址"); return } var req = URLRequest(url: url) req.httpMethod = "POST" req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.setValue(token, forHTTPHeaderField: "Authorization") let payload: [String: Any] = ["memberIcon": imageUrl] do { req.httpBody = try JSONSerialization.data(withJSONObject: payload) dbg_emitRequest(name: "更新头像URL", request: req, body: req.httpBody) } catch { raiseOops("请求创建失败"); return } showHud() URLSession.shared.dataTask(with: req) { [weak self] data, response, error in DispatchQueue.main.async { self?.dbg_emitResponse(name: "更新头像URL", data: data, response: response, error: error) self?.hideHud() if let error = error { self?.raiseOops("更新失败: \(error.localizedDescription)"); return } guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode), let data = data else { self?.raiseOops("服务器返回错误"); return } do { if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let codeStr: String = (json["code"] as? String) ?? ((json["code"] as? Int).map { String($0) }), codeStr == "200" { self?.vx_toast("头像更新成功") UserDefaults.standard.set(imageUrl, forKey: "memberIcon") NotificationCenter.default.post(name: Notification.Name("UserAvatarUpdated"), object: imageUrl) } else { self?.raiseOops("更新失败") } } catch { self?.raiseOops("响应解析失败") } } }.resume() } private func rx_updateNick(_ newNickname: String) { guard let token = UserDefaults.standard.string(forKey: "userToken") else { raiseOops("未找到用户凭证"); return } guard let url = URL(string: "\(baseURL)/petRecordApUser/modifyMemberName") else { raiseOops("无效的服务器地址"); return } var req = URLRequest(url: url) req.httpMethod = "POST" req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.setValue(token, forHTTPHeaderField: "Authorization") let payload: [String: Any] = ["memberName": newNickname] do { req.httpBody = try JSONSerialization.data(withJSONObject: payload) dbg_emitRequest(name: "修改昵称", request: req, body: req.httpBody) } catch { raiseOops("请求创建失败"); return } showHud() URLSession.shared.dataTask(with: req) { [weak self] data, response, error in DispatchQueue.main.async { self?.dbg_emitResponse(name: "修改昵称", data: data, response: response, error: error) self?.hideHud() if let error = error { self?.raiseOops("更新失败: \(error.localizedDescription)"); return } guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode), let data = data else { self?.raiseOops("服务器返回错误"); return } do { if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let codeStr: String = (json["code"] as? String) ?? ((json["code"] as? Int).map { String($0) }), codeStr == "200" { self?.zxNick = newNickname UserDefaults.standard.set(newNickname, forKey: "memberName") NotificationCenter.default.post(name: Notification.Name("UserNicknameUpdated"), object: newNickname) self?.grid.reloadData() self?.vx_toast("昵称修改成功") } else { self?.raiseOops("更新失败") } } catch { self?.raiseOops("响应解析失败") } } }.resume() } } // MARK: - Cell (obfuscated) final class VXSettingCell: UITableViewCell { static let reuseId = "vx.setting" private let tLabel: UILabel = { let l = UILabel() l.font = .systemFont(ofSize: 16, weight: .medium) l.textColor = .gray l.translatesAutoresizingMaskIntoConstraints = false return l }() private let vLabel: UILabel = { let l = UILabel() l.font = .systemFont(ofSize: 16, weight: .medium) l.textColor = UIColor(hex: "#999999") l.translatesAutoresizingMaskIntoConstraints = false return l }() private let head: UIImageView = { let iv = UIImageView() iv.contentMode = .scaleAspectFill iv.layer.cornerRadius = 25 iv.clipsToBounds = true iv.backgroundColor = UIColor(hex: "#F0F0F0") iv.translatesAutoresizingMaskIntoConstraints = false return iv }() private let arrow: UIImageView = { let iv = UIImageView() iv.image = UIImage(systemName: "chevron.right") iv.tintColor = UIColor(hex: "#000000") iv.translatesAutoresizingMaskIntoConstraints = false return iv }() private let line: UIView = { let v = UIView() v.backgroundColor = UIColor(hex: "#EEEEEE") v.translatesAutoresizingMaskIntoConstraints = false return v }() override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) contentView.backgroundColor = .white selectionStyle = .none contentView.addSubview(tLabel) contentView.addSubview(vLabel) contentView.addSubview(arrow) contentView.addSubview(line) contentView.addSubview(head) NSLayoutConstraint.activate([ tLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), tLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), arrow.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), arrow.widthAnchor.constraint(equalToConstant: 12), arrow.heightAnchor.constraint(equalToConstant: 18), vLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), vLabel.trailingAnchor.constraint(equalTo: arrow.leadingAnchor, constant: -8), head.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), head.trailingAnchor.constraint(equalTo: arrow.leadingAnchor, constant: -8), head.widthAnchor.constraint(equalToConstant: 50), head.heightAnchor.constraint(equalToConstant: 50), line.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), line.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), line.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), line.heightAnchor.constraint(equalToConstant: 0.5) ]) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func bind(title: String, value: String, isFirst: Bool, isLast: Bool, avatar: UIImage?, placeholder: String) { tLabel.text = title vLabel.isHidden = true head.isHidden = true if title == "头像修改" { head.isHidden = false head.image = avatar ?? UIImage(named: placeholder) ?? UIImage(named: "Home372") } else { vLabel.isHidden = false vLabel.text = value } line.isHidden = isLast } } // MARK: - Utils extension VKNimbusPanelController { fileprivate func vx_toast(_ message: String) { let lab = UILabel() lab.text = message lab.font = .systemFont(ofSize: 14) lab.textColor = .white lab.backgroundColor = UIColor.black.withAlphaComponent(0.7) lab.textAlignment = .center lab.alpha = 0 lab.layer.cornerRadius = 8 lab.clipsToBounds = true lab.translatesAutoresizingMaskIntoConstraints = false view.addSubview(lab) NSLayoutConstraint.activate([ lab.centerXAnchor.constraint(equalTo: view.centerXAnchor), lab.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -100), lab.widthAnchor.constraint(equalToConstant: 160), lab.heightAnchor.constraint(equalToConstant: 40) ]) UIView.animate(withDuration: 0.3, animations: { lab.alpha = 1 }) { _ in DispatchQueue.main.asyncAfter(deadline: .now() + 2) { UIView.animate(withDuration: 0.3, animations: { lab.alpha = 0 }) { _ in lab.removeFromSuperview() } } } } fileprivate func raiseOops(_ msg: String) { let ac = UIAlertController(title: "错误", message: msg, preferredStyle: .alert) ac.addAction(UIAlertAction(title: "确定", style: .default)) present(ac, animated: true) } } // MARK: - UIImage resize extension UIImage { fileprivate func x_resize(to size: CGSize) -> UIImage? { UIGraphicsBeginImageContextWithOptions(size, false, scale) defer { UIGraphicsEndImageContext() } draw(in: CGRect(origin: .zero, size: size)) return UIGraphicsGetImageFromCurrentImageContext() } } // MARK: - Debug loggers (obfuscated names) extension VKNimbusPanelController { func dbg_emitRequest(name: String, request: URLRequest, body: Data?) { #if DEBUG var lines: [String] = [] lines.append("\n====================[REQUEST: \(name)]====================") lines.append("URL : \(request.url?.absoluteString ?? "-")") lines.append("Method : \(request.httpMethod ?? "GET")") lines.append("Headers : \(request.allHTTPHeaderFields ?? [:])") if let ct = request.value(forHTTPHeaderField: "Content-Type"), ct.contains("multipart/form-data"), let body = body { lines.append("Body : size=\(body.count) bytes") } else if let body = body, !body.isEmpty { if let pretty = dbg_pretty(body) { lines.append("Body(JSON): \n\(pretty)") } else if let s = String(data: body, encoding: .utf8) { lines.append("Body(Text): \n\(s)") } else { lines.append("Body(Binary) size=\(body.count) bytes") } } else { lines.append("Body : ") } lines.append("cURL : \n\(dbg_curl(from: request, body: body))") lines.append("==========================================================\n") print(lines.joined(separator: "\n")) #endif } func dbg_emitResponse(name: String, data: Data?, response: URLResponse?, error: Error?) { #if DEBUG var lines: [String] = [] lines.append("\n--------------------[RESPONSE: \(name)]--------------------") if let http = response as? HTTPURLResponse { lines.append("Status : \(http.statusCode)") lines.append("URL : \(http.url?.absoluteString ?? "-")") lines.append("Headers : \(http.allHeaderFields)") } if let error = error { lines.append("Error : \(error.localizedDescription)") } if let d = data, !d.isEmpty { if let pretty = dbg_pretty(d) { lines.append("Body(JSON): \n\(pretty)") } else if let s = String(data: d, encoding: .utf8) { lines.append("Body(Text): \n\(s)") } else { lines.append("Body(Binary) size=\(d.count) bytes") } } else { lines.append("Body : ") } lines.append("----------------------------------------------------------\n") print(lines.joined(separator: "\n")) #endif } private func dbg_pretty(_ data: Data) -> String? { if let obj = try? JSONSerialization.jsonObject(with: data), let json = try? JSONSerialization.data(withJSONObject: obj, options: [.prettyPrinted]), let s = String(data: json, encoding: .utf8) { return s } return nil } private func dbg_curl(from request: URLRequest, body: Data?) -> String { var parts: [String] = ["curl -i"] if let m = request.httpMethod { parts.append("-X \(m)") } if let headers = request.allHTTPHeaderFields { for (k, v) in headers { parts.append("-H '\(k): \(v)'") } } if let body = body, !body.isEmpty, !(request.value(forHTTPHeaderField: "Content-Type")?.contains("multipart/form-data") ?? false) { if let s = String(data: body, encoding: .utf8) { let esc = s.replacingOccurrences(of: "'", with: "'\\''") parts.append("--data '\(esc)'") } } if let u = request.url?.absoluteString { parts.append("'\(u)'") } return parts.joined(separator: " ") } }