| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624 |
- //
- // LNAudioCallPanel.swift
- // Gami
- //
- // Created by OneeChan on 2026/2/1.
- //
- import Foundation
- import UIKit
- import SnapKit
- class LNAudioCallPanel: LNPopupView {
- private let background = UIImageView()
-
- private let avatar = UIImageView()
- private let nameLabel = UILabel()
-
- private let orderView = UIView()
- private let gameIc = UIImageView()
- private let orderStateLabel = UILabel()
- private let orderTimeLabel = UILabel()
- private let gameNameLabel = UILabel()
- private let gameCountLabel = UILabel()
-
- private let stateLabel = UILabel()
-
- private let onCallView = UIView()
- private let callOutView = UIView()
-
- private let callingView = UIView()
- private let durationLabel = UILabel()
- private let muteButton = UIButton()
- private let speakerButton = UIButton()
-
- private var timer: Timer?
-
- override init(frame: CGRect) {
- super.init(frame: frame)
-
- setupViews()
- LNEventDeliver.addObserver(self)
- }
-
- func toCallOut(uid: String) {
- callOutView.isHidden = false
- stateLabel.isHidden = false
- reloadUserInfo(uid: uid)
- }
-
- func onCallIn(uid: String) {
- onCallView.isHidden = false
- stateLabel.isHidden = false
- reloadUserInfo(uid: uid)
- }
-
- func resume() {
- guard let callInfo = LNIMManager.shared.curCallInfo else { return }
- if callInfo.beginTime > 0 {
- updateCallDuration()
- startTimer()
- callingView.isHidden = false
- stateLabel.isHidden = true
- getCurOrders(uid: callInfo.uid)
- } else if callInfo.isInCome {
- onCallView.isHidden = false
- stateLabel.isHidden = false
- } else {
- callOutView.isHidden = false
- stateLabel.isHidden = false
- }
-
- reloadUserInfo(uid: callInfo.uid)
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- }
- extension LNAudioCallPanel {
- private func reloadUserInfo(uid: String) {
- LNProfileManager.shared.getUserProfile(uid: uid) { [weak self] info in
- guard let self else { return }
- guard let info else { return }
- background.sd_setImage(with: URL(string: info.avatar))
- avatar.sd_setImage(with: URL(string: info.avatar))
- nameLabel.text = info.nickname
- }
- }
-
- private func getCurOrders(uid: String) {
- LNOrderManager.shared.getUnfinishedOrderWith(uid: uid, size: 1, next: nil) { [weak self] list, _ in
- guard let self else { return }
- guard let order = list?.first else { return }
-
- avatar.snp.updateConstraints { make in
- make.width.height.equalTo(120)
- }
- orderView.isHidden = false
- gameIc.sd_setImage(with: URL(string: order.categoryIcon))
- gameNameLabel.text = order.bizCategoryName
- orderTimeLabel.text = Double(order.createTime / 1_000).formattedFullDateWithTime()
- gameCountLabel.text = "x \(order.purchaseQty) \(order.unit)"
- orderStateLabel.text = order.statusUI.title
- }
- }
-
- private func updateCallDuration() {
- guard let callInfo = LNIMManager.shared.curCallInfo else { return }
- let duration = curTime - callInfo.beginTime
- durationLabel.text = duration.timeCountDisplay
- }
-
- private func startTimer() {
- stopTimer()
- let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
- guard let self else { return }
- guard LNIMManager.shared.curCallInfo != nil else {
- stopTimer()
- return
- }
-
- updateCallDuration()
- }
- RunLoop.main.add(timer, forMode: .common)
- self.timer = timer
- }
-
- private func stopTimer() {
- timer?.invalidate()
- timer = nil
- }
- }
- extension LNAudioCallPanel: LNIMManagerNotify {
- func onVoiceCallBegin() {
- onCallView.isHidden = true
- callOutView.isHidden = true
- callingView.isHidden = false
- stateLabel.isHidden = true
-
- updateCallDuration()
- startTimer()
-
- if let callInfo = LNIMManager.shared.curCallInfo {
- getCurOrders(uid: callInfo.uid)
- }
- }
-
- func onVoiceCallEnd() {
- dismiss()
- stopTimer()
- }
-
- func onVoiceCallInfoChanged() {
- guard let callInfo = LNIMManager.shared.curCallInfo else { return }
- muteButton.setImage(callInfo.isMute ? .icCallMute : .icCallUnmute, for: .normal)
- let icon: UIImage = if callInfo.deviceType == .earpiece {
- .icCallSpeakerEarpiece
- } else {
- .icCallSpeakerPhone
- }
- speakerButton.setImage(icon, for: .normal)
- }
- }
- extension LNAudioCallPanel {
- private func setupViews() {
- containerHeight = .percent(1.0)
-
- let background = buildBackground()
- container.addSubview(background)
- background.snp.makeConstraints { make in
- make.edges.equalToSuperview()
- }
-
- let navBar = buildNavBar()
- container.addSubview(navBar)
- navBar.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalToSuperview()
- }
-
- let infoView = buildInfoView()
- container.addSubview(infoView)
- infoView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview().inset(16)
- make.top.greaterThanOrEqualTo(navBar.snp.bottom).offset(16)
- make.bottom.equalTo(container.snp.centerY).offset(-30).priority(.medium)
- }
-
- stateLabel.text = .init(key: "C00015")
- stateLabel.font = .body_xl
- stateLabel.textColor = .text_2
- container.addSubview(stateLabel)
- stateLabel.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.top.equalTo(infoView.snp.bottom)
- }
-
- let onCallView = buildOnCallView()
- container.addSubview(onCallView)
- onCallView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview().inset(48)
- make.bottom.equalToSuperview().offset(-100)
- }
-
- let callOutView = buildCallOutView()
- container.addSubview(callOutView)
- callOutView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview().inset(48)
- make.bottom.equalToSuperview().offset(-100)
- }
-
- let callingView = buildCallingView()
- container.addSubview(callingView)
- callingView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview().inset(48)
- make.bottom.equalToSuperview().offset(-100)
- }
- }
-
- private func buildBackground() -> UIView {
- background.backgroundColor = .lightGray
- background.contentMode = .scaleAspectFill
-
- let blurEffect = UIBlurEffect(style: .light)
- let blurView = UIVisualEffectView(effect: blurEffect)
- background.addSubview(blurView)
- blurView.snp.makeConstraints { make in
- make.edges.equalToSuperview()
- }
-
- // 可选:添加半透明遮罩,增强模糊层次感(毛玻璃常用搭配)
- let maskView = UIView(frame: blurView.bounds)
- maskView.backgroundColor = UIColor.black.withAlphaComponent(0.3) // 0.1~0.3为宜
- blurView.contentView.addSubview(maskView)
- maskView.snp.makeConstraints { make in
- make.edges.equalToSuperview()
- }
-
- return background
- }
-
- private func buildNavBar() -> UIView {
- let navBar = LNFakeNaviBar()
-
- let minButton = UIButton()
- minButton.setImage(.icCallMin, for: .normal)
- minButton.addAction(UIAction(handler: { [weak self] _ in
- guard let self else { return }
- dismiss()
- }), for: .touchUpInside)
- navBar.actionView.addSubview(minButton)
- minButton.snp.makeConstraints { make in
- make.leading.equalToSuperview().offset(16)
- make.centerY.equalToSuperview()
- make.width.height.equalTo(38)
- }
-
- return navBar
- }
-
- private func buildInfoView() -> UIView {
- let container = UIView()
-
- let stackView = UIStackView()
- stackView.axis = .vertical
- stackView.alignment = .center
- stackView.spacing = 4
- container.addSubview(stackView)
- stackView.snp.makeConstraints { make in
- make.edges.equalToSuperview()
- }
-
- avatar.layer.cornerRadius = 75
- avatar.clipsToBounds = true
- avatar.snp.makeConstraints { make in
- make.width.height.equalTo(150)
- }
- stackView.addArrangedSubview(avatar)
-
- nameLabel.font = .heading_h1
- nameLabel.textColor = .text_1
- stackView.addArrangedSubview(nameLabel)
-
- let orderView = buildOrderView()
- stackView.addArrangedSubview(orderView)
- orderView.snp.makeConstraints { make in
- make.width.equalToSuperview()
- }
-
- return container
- }
-
- private func buildOrderView() -> UIView {
- orderView.backgroundColor = .fill.withAlphaComponent(0.5)
- orderView.layer.cornerRadius = 12
- orderView.isHidden = true
-
- orderView.addSubview(gameIc)
- gameIc.snp.makeConstraints { make in
- make.top.equalToSuperview()
- make.leading.equalToSuperview().offset(10)
- make.width.height.equalTo(50)
- }
-
- let infoView = UIView()
- orderView.addSubview(infoView)
- infoView.snp.makeConstraints { make in
- make.centerY.equalTo(gameIc)
- make.leading.equalTo(gameIc.snp.trailing).offset(2)
- make.trailing.equalToSuperview().offset(-10)
- }
-
- orderStateLabel.font = .heading_h4
- orderStateLabel.textColor = .text_5
- infoView.addSubview(orderStateLabel)
- orderStateLabel.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalToSuperview()
- }
-
- orderTimeLabel.font = .body_xs
- orderTimeLabel.textColor = .text_4
- infoView.addSubview(orderTimeLabel)
- orderTimeLabel.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.bottom.equalToSuperview()
- make.top.equalTo(orderStateLabel.snp.bottom).offset(2)
- }
-
- let line = UIView()
- line.backgroundColor = .fill_4
- orderView.addSubview(line)
- line.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview().inset(12)
- make.height.equalTo(0.5)
- make.bottom.equalTo(gameIc)
- }
-
- let gameInfo = UIView()
- orderView.addSubview(gameInfo)
- gameInfo.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalTo(gameIc.snp.bottom)
- make.bottom.equalToSuperview()
- make.height.equalTo(30)
- }
-
- gameNameLabel.font = .body_s
- gameNameLabel.textColor = .text_4
- gameInfo.addSubview(gameNameLabel)
- gameNameLabel.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.leading.equalToSuperview().offset(16)
- }
-
- gameCountLabel.font = .body_s
- gameCountLabel.textColor = .text_4
- gameCountLabel.setContentHuggingPriority(.required, for: .horizontal)
- gameCountLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
- gameInfo.addSubview(gameCountLabel)
- gameCountLabel.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.trailing.equalToSuperview().offset(-16)
- make.leading.greaterThanOrEqualTo(gameNameLabel.snp.trailing).offset(16)
- }
-
- return orderView
- }
-
- private func buildOnCallView() -> UIView {
- onCallView.isHidden = true
-
- let stackView = UIStackView()
- stackView.distribution = .equalSpacing
- onCallView.addSubview(stackView)
- stackView.snp.makeConstraints { make in
- make.edges.equalToSuperview()
- }
-
- let rejectButton = UIButton()
- rejectButton.setImage(.icCallDecline, for: .normal)
- rejectButton.addAction(UIAction(handler: { _ in
- LNIMManager.shared.rejectVoiceCall()
- }), for: .touchUpInside)
- stackView.addArrangedSubview(rejectButton)
-
- let acceptButton = UIButton()
- acceptButton.setImage(.icCallAccept, for: .normal)
- acceptButton.addAction(UIAction(handler: { _ in
- LNIMManager.shared.acceptVoiceCall()
- }), for: .touchUpInside)
- stackView.addArrangedSubview(acceptButton)
-
- return onCallView
- }
-
- private func buildCallOutView() -> UIView {
- callOutView.isHidden = true
-
- let cancelButton = UIButton()
- cancelButton.setImage(.icCallDecline, for: .normal)
- cancelButton.addAction(UIAction(handler: { _ in
- LNIMManager.shared.hangupVoiceCall()
- }), for: .touchUpInside)
- callOutView.addSubview(cancelButton)
- cancelButton.snp.makeConstraints { make in
- make.verticalEdges.equalToSuperview()
- make.centerX.equalToSuperview()
- }
-
- return callOutView
- }
-
- private func buildCallingView() -> UIView {
- callingView.isHidden = true
-
- let stackView = UIStackView()
- stackView.alignment = .center
- stackView.distribution = .equalSpacing
- callingView.addSubview(stackView)
- stackView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.bottom.equalToSuperview()
- }
-
- muteButton.setImage(.icCallUnmute, for: .normal)
- muteButton.addAction(UIAction(handler: { _ in
- LNIMManager.shared.switchVoiceCallMicrophone()
- }), for: .touchUpInside)
- stackView.addArrangedSubview(muteButton)
-
- let hangupButton = UIButton()
- hangupButton.setImage(.icCallDecline, for: .normal)
- hangupButton.addAction(UIAction(handler: { _ in
- LNIMManager.shared.hangupVoiceCall()
- }), for: .touchUpInside)
- stackView.addArrangedSubview(hangupButton)
-
- speakerButton.setImage(.icCallSpeakerEarpiece, for: .normal)
- speakerButton.addAction(UIAction(handler: { [weak self] _ in
- guard let self else { return }
- // if DevicesUtil.isBluetoothHeadsetConnected {
- // let menu = LNVoiceCallSpeakerSelectPopoverMenu()
- // menu.pointAt(parentView: self, targetView: speakerButton) { type in
- // LNIMManager.shared.switchVoiceCallSpeakerType(type: type)
- // }
- // } else
- if let callInfo = LNIMManager.shared.curCallInfo {
- if callInfo.deviceType == .speakerphone {
- LNIMManager.shared.switchVoiceCallSpeakerType(type: .earpiece)
- } else {
- LNIMManager.shared.switchVoiceCallSpeakerType(type: .speakerphone)
- }
- }
- }), for: .touchUpInside)
- stackView.addArrangedSubview(speakerButton)
-
- durationLabel.font = .body_l
- durationLabel.textColor = .text_1
- callingView.addSubview(durationLabel)
- durationLabel.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.top.equalToSuperview()
- make.bottom.equalTo(stackView.snp.top).offset(-14)
- }
-
- return callingView
- }
- }
- private class LNVoiceCallSpeakerSelectPopoverMenu: UIView {
- private let holder = UIView()
- private var handler: ((LNIMVoiceCallSpeakerType) -> Void)?
-
- override init(frame: CGRect) {
- super.init(frame: frame)
-
- let touchView = UIView()
- touchView.onTap { [weak self] in
- guard let self else { return }
- dismiss()
- }
- addSubview(touchView)
- touchView.snp.makeConstraints { make in
- make.edges.equalToSuperview()
- }
-
- holder.backgroundColor = .fill.withAlphaComponent(0.8)
- holder.layer.cornerRadius = 10
- addSubview(holder)
-
- let stackView = UIStackView()
- stackView.axis = .vertical
- holder.addSubview(stackView)
- stackView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview().inset(4)
- make.top.equalToSuperview().offset(4)
- make.bottom.equalToSuperview().offset(-10)
- }
-
- let device = LNIMManager.shared.curCallInfo?.deviceType
-
- let bluetooth = buildMenuItem(ic: .icCallDeviceBluetooth, title: .init(key: "C00008"), isCheck: device == .bluetooth)
- bluetooth.onTap { [weak self] in
- guard let self else { return }
- dismiss()
- handler?(.bluetooth)
- }
- stackView.addArrangedSubview(bluetooth)
-
- let ear = buildMenuItem(ic: .icCallDeviceEar, title: .init(key: "C00006"), isCheck: device == .earpiece)
- ear.onTap { [weak self] in
- guard let self else { return }
- dismiss()
- handler?(.earpiece)
- }
- stackView.addArrangedSubview(ear)
-
- let speaker = buildMenuItem(ic: .icCallDeviceSpeaker, title: .init(key: "C00007"), isCheck: device == .speakerphone)
- speaker.onTap { [weak self] in
- guard let self else { return }
- dismiss()
- handler?(.speakerphone)
- }
- stackView.addArrangedSubview(speaker)
-
- let triangle = UIImageView()
- triangle.image = .icTriangleDown
- triangle.alpha = 0.8
- addSubview(triangle)
- triangle.snp.makeConstraints { make in
- make.centerX.equalTo(holder)
- make.top.equalTo(holder.snp.bottom)
- }
- }
-
- func pointAt(parentView: UIView, targetView: UIView, handler: @escaping (LNIMVoiceCallSpeakerType) -> Void) {
- parentView.addSubview(self)
- snp.makeConstraints { make in
- make.edges.equalToSuperview()
- }
-
- holder.snp.makeConstraints { make in
- make.centerX.equalTo(targetView)
- make.bottom.equalTo(targetView.snp.top).offset(-8)
- }
-
- self.handler = handler
- }
-
- func dismiss() {
- removeFromSuperview()
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- private func buildMenuItem(ic: UIImage, title: String, isCheck: Bool) -> UIView {
- let container = UIView()
- container.snp.makeConstraints { make in
- make.height.equalTo(38)
- make.width.equalTo(124)
- }
-
- let icon = UIImageView(image: ic)
- container.addSubview(icon)
- icon.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.leading.equalToSuperview().offset(6)
- }
-
- let titleLabel = UILabel()
- titleLabel.font = .body_m
- titleLabel.textColor = isCheck ? .text_5 : .text_4
- titleLabel.text = title
- container.addSubview(titleLabel)
- titleLabel.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.leading.equalTo(icon.snp.trailing).offset(8)
- }
-
- if isCheck {
- let checkIc = UIImageView(image: .icCheckBlack)
- container.addSubview(checkIc)
- checkIc.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.trailing.equalToSuperview().offset(-6)
- make.leading.greaterThanOrEqualTo(titleLabel.snp.trailing).offset(2)
- }
- }
-
- return container
- }
- }
- #if DEBUG
- import SwiftUI
- struct LNVoiceCallPanelPreview: UIViewRepresentable {
- func makeUIView(context: Context) -> some UIView {
- let container = UIView()
- container.backgroundColor = .lightGray
-
- let view = LNAudioCallPanel()
- view.popup()
-
- return container
- }
-
- func updateUIView(_ uiView: UIViewType, context: Context) { }
- }
- #Preview(body: {
- LNVoiceCallPanelPreview()
- })
- #endif
|