增加换肤功能
This commit is contained in:
321
TUIKit/TUIRoomKit/Source/View/Page/ConferenceMainView.swift
Normal file
321
TUIKit/TUIRoomKit/Source/View/Page/ConferenceMainView.swift
Normal file
@@ -0,0 +1,321 @@
|
||||
//
|
||||
// ConferenceMainView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by aby on 2022/12/27.
|
||||
// Copyright © 2022 Tencent. All rights reserved.
|
||||
// The main conference interface is responsible for arranging and managing the top bar, bottom bar, video interface, etc.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RTCRoomEngine
|
||||
import Factory
|
||||
|
||||
protocol ConferenceMainViewFactory {
|
||||
func makeBottomView() -> BottomView
|
||||
func makeTopView() -> TopView
|
||||
func makeVideoSeatView() -> UIView
|
||||
func makeRaiseHandNoticeView() -> UIView
|
||||
func makeLocalAudioView() -> UIView
|
||||
func makeWaterMarkLayer() -> WaterMarkLayer
|
||||
func makeFloatChatButton() -> FloatChatButton
|
||||
func makeFloatChatDisplayView() -> FloatChatDisplayView
|
||||
func makeRaiseHandApplicationNotificationView() -> RaiseHandApplicationNotificationView
|
||||
func makeConferencePasswordView() -> ConferencePasswordView
|
||||
}
|
||||
|
||||
struct ConferenceMainViewLayout { //Layout changes when switching between horizontal and vertical screens
|
||||
let bottomViewLandscapeSpace: Float = 0
|
||||
let bottomViewPortraitSpace: Float = 34.0
|
||||
let topViewLandscapeHight: Float = 75.0
|
||||
let topViewPortraitHight: Float = 105.0
|
||||
let videoSeatViewPortraitSpace: Float = 73.0
|
||||
let videoSeatViewLandscapeSpace: Float = 82.0
|
||||
}
|
||||
|
||||
class ConferenceMainView: UIView {
|
||||
let viewModel: ConferenceMainViewModel
|
||||
let viewFactory: ConferenceMainViewFactory
|
||||
let layout: ConferenceMainViewLayout = ConferenceMainViewLayout()
|
||||
@Injected(\.navigation) private var route
|
||||
init(viewModel: ConferenceMainViewModel,
|
||||
viewFactory: ConferenceMainViewFactory) {
|
||||
self.viewModel = viewModel
|
||||
self.viewFactory = viewFactory
|
||||
super.init(frame: .zero)
|
||||
viewModel.viewResponder = self
|
||||
subscribeUIEvent()
|
||||
}
|
||||
private var currentLandscape: Bool = isLandscape
|
||||
private let firstDelayDisappearanceTime = 6.0
|
||||
private let delayDisappearanceTime = 3.0
|
||||
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
lazy var topView: TopView = {
|
||||
return viewFactory.makeTopView()
|
||||
}()
|
||||
|
||||
lazy var videoSeatView: UIView = {
|
||||
return viewFactory.makeVideoSeatView()
|
||||
}()
|
||||
|
||||
lazy var bottomView: BottomView = {
|
||||
return viewFactory.makeBottomView()
|
||||
}()
|
||||
|
||||
lazy var raiseHandNoticeView: UIView = {
|
||||
return viewFactory.makeRaiseHandNoticeView()
|
||||
}()
|
||||
|
||||
lazy var localAudioView: UIView = {
|
||||
return viewFactory.makeLocalAudioView()
|
||||
}()
|
||||
|
||||
lazy var waterMarkLayer: CALayer = {
|
||||
return viewFactory.makeWaterMarkLayer()
|
||||
}()
|
||||
|
||||
lazy var floatChatDisplayView: FloatChatDisplayView = {
|
||||
return viewFactory.makeFloatChatDisplayView()
|
||||
}()
|
||||
|
||||
lazy var floatChatButton: FloatChatButton = {
|
||||
return viewFactory.makeFloatChatButton()
|
||||
}()
|
||||
|
||||
lazy var raiseHandApplicationNotificationView: RaiseHandApplicationNotificationView = {
|
||||
let applicationNotificationView = viewFactory.makeRaiseHandApplicationNotificationView()
|
||||
return applicationNotificationView
|
||||
}()
|
||||
|
||||
lazy var conferencePasswordView: ConferencePasswordView = {
|
||||
return viewFactory.makeConferencePasswordView()
|
||||
}()
|
||||
|
||||
// MARK: - view layout
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
backgroundColor = UIColor(0x0F1014)
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
guard currentLandscape != isLandscape else { return }
|
||||
setupRootViewOrientation(isLandscape: isLandscape)
|
||||
currentLandscape = isLandscape
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(videoSeatView)
|
||||
if viewModel.isShownWaterMark {
|
||||
layer.addSublayer(waterMarkLayer)
|
||||
}
|
||||
addSubview(topView)
|
||||
addSubview(floatChatDisplayView)
|
||||
addSubview(floatChatButton)
|
||||
addSubview(bottomView)
|
||||
addSubview(localAudioView)
|
||||
addSubview(raiseHandNoticeView)
|
||||
addSubview(raiseHandApplicationNotificationView)
|
||||
addSubview(conferencePasswordView)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
setupRootViewOrientation(isLandscape: isLandscape)
|
||||
raiseHandNoticeView.snp.makeConstraints { make in
|
||||
make.bottom.equalTo(bottomView.snp.top).offset(-15)
|
||||
make.centerX.equalToSuperview()
|
||||
make.height.equalTo(40)
|
||||
make.width.equalTo(300)
|
||||
}
|
||||
localAudioView.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.width.height.equalTo(40.scale375())
|
||||
make.bottom.equalToSuperview().offset(-40.scale375Height())
|
||||
}
|
||||
floatChatButton.snp.makeConstraints { make in
|
||||
make.bottom.equalTo(localAudioView.snp.top).offset(-18)
|
||||
make.height.equalTo(30)
|
||||
make.leading.equalTo(videoSeatView.snp.leading)
|
||||
}
|
||||
floatChatDisplayView.snp.makeConstraints { make in
|
||||
make.bottom.equalTo(floatChatButton.snp.top).offset(-8)
|
||||
make.height.equalTo(128)
|
||||
make.leading.equalToSuperview().offset(5)
|
||||
make.width.equalTo(313)
|
||||
}
|
||||
raiseHandApplicationNotificationView.snp.makeConstraints { make in
|
||||
make.top.equalTo(topView.snp.bottom)
|
||||
make.width.equalTo(359.scale375())
|
||||
make.centerX.equalToSuperview()
|
||||
make.height.equalTo(40.scale375Height())
|
||||
}
|
||||
conferencePasswordView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
private func bindInteraction() {
|
||||
perform(#selector(hideToolBar),with: nil,afterDelay: firstDelayDisappearanceTime)
|
||||
}
|
||||
|
||||
func setupRootViewOrientation(isLandscape: Bool) {
|
||||
videoSeatView.snp.remakeConstraints { make in
|
||||
if isLandscape {
|
||||
make.leading.equalTo(layout.videoSeatViewLandscapeSpace)
|
||||
make.trailing.equalTo(-layout.videoSeatViewLandscapeSpace)
|
||||
make.top.bottom.equalToSuperview()
|
||||
} else {
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.top.equalTo(layout.videoSeatViewPortraitSpace)
|
||||
make.bottom.equalTo(-layout.videoSeatViewPortraitSpace)
|
||||
}
|
||||
}
|
||||
topView.snp.remakeConstraints() { make in
|
||||
make.top.equalToSuperview()
|
||||
make.leading.equalTo(safeAreaLayoutGuide.snp.leading)
|
||||
make.trailing.equalTo(safeAreaLayoutGuide.snp.trailing)
|
||||
if isLandscape {
|
||||
make.height.equalTo(layout.topViewLandscapeHight)
|
||||
} else {
|
||||
make.height.equalTo(layout.topViewPortraitHight)
|
||||
}
|
||||
}
|
||||
bottomView.snp.remakeConstraints { make in
|
||||
make.leading.equalTo(safeAreaLayoutGuide.snp.leading)
|
||||
make.trailing.equalTo(safeAreaLayoutGuide.snp.trailing)
|
||||
make.height.equalTo(bottomView.isUnfold ? bottomView.unfoldHeight : bottomView.packUpHeight)
|
||||
if isLandscape {
|
||||
make.bottom.equalToSuperview().offset(-layout.bottomViewLandscapeSpace)
|
||||
} else {
|
||||
make.bottom.equalToSuperview().offset(-layout.bottomViewPortraitSpace)
|
||||
}
|
||||
}
|
||||
topView.updateRootViewOrientation(isLandscape: isLandscape)
|
||||
setupWaterMarkLayerOrientation(isLandscape: isLandscape)
|
||||
}
|
||||
|
||||
private func setupWaterMarkLayerOrientation(isLandscape: Bool) {
|
||||
guard viewModel.isShownWaterMark else { return }
|
||||
let widthSpace = isLandscape ? CGFloat(layout.videoSeatViewLandscapeSpace) : 0
|
||||
let heightSpace = isLandscape ? 0 : CGFloat(layout.videoSeatViewPortraitSpace)
|
||||
waterMarkLayer.frame = CGRect(x: widthSpace, y: heightSpace, width: kScreenWidth - widthSpace * 2, height: kScreenHeight - heightSpace * 2)
|
||||
waterMarkLayer.setNeedsDisplay()
|
||||
}
|
||||
|
||||
private func subscribeUIEvent() {
|
||||
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_ShowFloatChatView, responder: self)
|
||||
}
|
||||
|
||||
private func unsubscribeEvent() {
|
||||
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_ShowFloatChatView, responder: self)
|
||||
}
|
||||
|
||||
deinit {
|
||||
NSObject.cancelPreviousPerformRequests(withTarget: self)
|
||||
unsubscribeEvent()
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
|
||||
extension ConferenceMainView: ConferenceMainViewResponder {
|
||||
func hidePasswordView() {
|
||||
conferencePasswordView.hide()
|
||||
}
|
||||
|
||||
func showPasswordView(roomId: String) {
|
||||
conferencePasswordView.show(roomId: roomId)
|
||||
}
|
||||
|
||||
func showExitRoomView() {
|
||||
let view = ExitRoomView(viewModel: ExitRoomViewModel())
|
||||
view.show(rootView: self)
|
||||
}
|
||||
|
||||
func showAlert(title: String?, message: String?, sureTitle:String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?) {
|
||||
RoomRouter.presentAlert(title: title, message: message, sureTitle: sureTitle, declineTitle: declineTitle, sureBlock: sureBlock, declineBlock: declineBlock)
|
||||
}
|
||||
|
||||
func showAlertWithAutoConfirm(title: String?, message: String?, sureTitle:String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?, autoConfirmSeconds: Int?) {
|
||||
RoomRouter.presentAlert(title: title, message: message, sureTitle: sureTitle, declineTitle: declineTitle, sureBlock: sureBlock, declineBlock: declineBlock, autoConfirmSeconds: autoConfirmSeconds)
|
||||
}
|
||||
|
||||
func makeToast(text: String) {
|
||||
RoomRouter.makeToastInCenter(toast: text, duration: 1)
|
||||
}
|
||||
|
||||
func showRaiseHandNoticeView() {
|
||||
raiseHandNoticeView.isHidden = false
|
||||
}
|
||||
|
||||
func updateRoomInfo(roomInfo: TUIRoomInfo) {
|
||||
floatChatButton.updateRoomId(roomId: roomInfo.roomId)
|
||||
}
|
||||
|
||||
private func showToolBar() {
|
||||
topView.alpha = 1
|
||||
bottomView.alpha = 1
|
||||
topView.isHidden = false
|
||||
bottomView.isHidden = false
|
||||
viewModel.hideLocalAudioView()
|
||||
}
|
||||
|
||||
@objc private func hideToolBar() {
|
||||
topView.alpha = 0
|
||||
bottomView.alpha = 0
|
||||
topView.isHidden = true
|
||||
bottomView.isHidden = true
|
||||
viewModel.showLocalAudioView()
|
||||
}
|
||||
|
||||
func changeToolBarHiddenState() {
|
||||
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hideToolBar), object: nil)
|
||||
if topView.isHidden {
|
||||
showToolBar()
|
||||
perform(#selector(hideToolBar),with: nil,afterDelay: delayDisappearanceTime)
|
||||
} else if !bottomView.isUnfold {
|
||||
hideToolBar()
|
||||
}
|
||||
}
|
||||
|
||||
func setToolBarDelayHidden(isDelay: Bool) {
|
||||
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hideToolBar), object: nil)
|
||||
guard !bottomView.isUnfold, isDelay else { return }
|
||||
perform(#selector(hideToolBar),with: nil,afterDelay: delayDisappearanceTime)
|
||||
}
|
||||
|
||||
func showRepeatJoinRoomAlert() {
|
||||
let sureAction = UIAlertAction(title: .repeatJoinRoomSureText, style: .default) { _ in
|
||||
}
|
||||
let alertState = AlertState(title: .repeatJoinRoomTitle, message: .repeatJoinRoomMessage, sureAction: sureAction, declineAction: nil)
|
||||
route.present(route: .alert(state: alertState))
|
||||
}
|
||||
}
|
||||
|
||||
extension ConferenceMainView: RoomKitUIEventResponder {
|
||||
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
|
||||
switch key {
|
||||
case .TUIRoomKitService_ShowFloatChatView:
|
||||
guard let shouldShow = info?["shouldShow"] as? Bool else { return }
|
||||
floatChatButton.isHidden = !shouldShow
|
||||
floatChatDisplayView.isHidden = !shouldShow
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static let repeatJoinRoomTitle = localized("Currently in the room")
|
||||
static let repeatJoinRoomMessage = localized("Please exit before joining a new room")
|
||||
static let repeatJoinRoomSureText = localized("I see")
|
||||
}
|
||||
442
TUIKit/TUIRoomKit/Source/View/Page/RoomRouter.swift
Normal file
442
TUIKit/TUIRoomKit/Source/View/Page/RoomRouter.swift
Normal file
@@ -0,0 +1,442 @@
|
||||
//
|
||||
// RoomRouter.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2022/9/30.
|
||||
// Copyright © 2022 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import TUICore
|
||||
import RTCRoomEngine
|
||||
|
||||
// View routing context
|
||||
class RouteContext {
|
||||
var rootNavigation: UINavigationController?
|
||||
typealias Weak<T> = () -> T?
|
||||
var alterControllers: [Weak<UIViewController>] = []
|
||||
var popUpViewController: Weak<PopUpViewController>?
|
||||
var appearance: AnyObject?
|
||||
let navigationDelegate = RoomRouter.RoomNavigationDelegate()
|
||||
var currentLandscape: Bool = isLandscape
|
||||
weak var rootViewController: UIViewController?
|
||||
var chatWindow : UIWindow?
|
||||
let chatWindowWidth = min(kScreenWidth, kScreenHeight) + 20
|
||||
let chatWindowHeight = min(kScreenWidth, kScreenHeight)
|
||||
init() {
|
||||
if #available(iOS 13, *) {
|
||||
appearance = UINavigationBarAppearance()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RoomRouter: NSObject {
|
||||
static let shared = RoomRouter()
|
||||
private let context: RouteContext = RouteContext()
|
||||
private override init() {
|
||||
super.init()
|
||||
subscribeUIEvent()
|
||||
}
|
||||
|
||||
class RoomNavigationDelegate: NSObject {
|
||||
|
||||
}
|
||||
|
||||
var navController: UINavigationController? {
|
||||
return context.rootNavigation
|
||||
}
|
||||
|
||||
private func subscribeUIEvent() {
|
||||
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_ShowRoomMainView, responder: self)
|
||||
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_ShowRoomVideoFloatView, responder: self)
|
||||
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_HiddenChatWindow, responder: self)
|
||||
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, responder: self)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleDeviceOrientationChange), name: UIDevice.orientationDidChangeNotification, object: nil)
|
||||
}
|
||||
|
||||
private func unsubscribeEvent() {
|
||||
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_ShowRoomMainView, responder: self)
|
||||
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_ShowRoomVideoFloatView, responder: self)
|
||||
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_HiddenChatWindow, responder: self)
|
||||
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, responder: self)
|
||||
NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
|
||||
}
|
||||
|
||||
func hasChatWindow() -> Bool {
|
||||
return context.chatWindow != nil
|
||||
}
|
||||
|
||||
func pushToChatController(user: UserEntity, roomInfo: TUIRoomInfo) {
|
||||
guard let chatVC = makeChatController(user: user, roomInfo: roomInfo) else { return }
|
||||
if !isLandscape {
|
||||
push(viewController: chatVC, animated: false)
|
||||
} else {
|
||||
let nav = UINavigationController(rootViewController: chatVC)
|
||||
nav.navigationBar.backgroundColor = .white
|
||||
if #available(iOS 13, *) {
|
||||
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return }
|
||||
context.chatWindow = UIWindow(windowScene: windowScene)
|
||||
} else {
|
||||
context.chatWindow = UIWindow(frame: UIScreen.main.bounds)
|
||||
}
|
||||
context.chatWindow?.frame = CGRect(x: kScreenWidth - context.chatWindowWidth - kDeviceSafeBottomHeight, y: 0, width: context.chatWindowWidth, height: context.chatWindowHeight)
|
||||
context.chatWindow?.rootViewController = nav
|
||||
context.chatWindow?.windowLevel = UIWindow.Level.statusBar + 1
|
||||
context.chatWindow?.isHidden = false
|
||||
context.chatWindow?.makeKeyAndVisible()
|
||||
}
|
||||
}
|
||||
|
||||
func makeChatController(user: UserEntity, roomInfo: TUIRoomInfo) -> UIViewController? {
|
||||
let config: [String : Any] = [
|
||||
TUICore_TUIChatService_SetChatExtensionMethod_EnableVideoCallKey: false,
|
||||
TUICore_TUIChatService_SetChatExtensionMethod_EnableAudioCallKey: false,
|
||||
TUICore_TUIChatService_SetChatExtensionMethod_EnableLinkKey: false,
|
||||
]
|
||||
TUICore.callService(TUICore_TUIChatService, method: TUICore_TUIChatService_SetChatExtensionMethod, param: config)
|
||||
let maxSizeKey = "TUICore_TUIChatService_SetMaxTextSize"
|
||||
let chatWidth = min(kScreenWidth, kScreenHeight)
|
||||
let sizeParam : [String : Any] = ["maxsize": CGSize(width: chatWidth - 150, height: Double(MAXFLOAT))]
|
||||
TUICore.callService(TUICore_TUIChatService, method: maxSizeKey, param: sizeParam)
|
||||
let param : [String : Any] = [
|
||||
TUICore_TUIChatObjectFactory_ChatViewController_Title : String.chatText,
|
||||
TUICore_TUIChatObjectFactory_ChatViewController_GroupID: roomInfo.roomId,
|
||||
TUICore_TUIChatObjectFactory_ChatViewController_AvatarUrl : user.avatarUrl,
|
||||
TUICore_TUIChatObjectFactory_ChatViewController_Enable_Video_Call : String(0),
|
||||
TUICore_TUIChatObjectFactory_ChatViewController_Enable_Audio_Call : String(0),
|
||||
TUICore_TUIChatObjectFactory_ChatViewController_Enable_Room : String(0),
|
||||
TUICore_TUIChatObjectFactory_ChatViewController_Limit_Portrait_Orientation: String(1),
|
||||
TUICore_TUIChatObjectFactory_ChatViewController_Enable_Poll : String(0),
|
||||
TUICore_TUIChatObjectFactory_ChatViewController_Enable_GroupNote : String(0),
|
||||
TUICore_TUIChatObjectFactory_ChatViewController_Enable_WelcomeCustomMessage :String(0),
|
||||
TUICore_TUIChatObjectFactory_ChatViewController_Enable_TakePhoto :String(0),
|
||||
TUICore_TUIChatObjectFactory_ChatViewController_Enable_RecordVideo :String(0),
|
||||
]
|
||||
return TUICore.createObject(TUICore_TUIChatObjectFactory, key: TUICore_TUIChatObjectFactory_ChatViewController_Classic,
|
||||
param: param) as? UIViewController
|
||||
}
|
||||
|
||||
func pushMainViewController() {
|
||||
let vc = makeMainViewController()
|
||||
push(viewController: vc)
|
||||
}
|
||||
|
||||
func presentPopUpViewController(viewType: PopUpViewType, height: CGFloat, backgroundColor: UIColor = UIColor(0x1B1E26)) {
|
||||
if let observer = context.popUpViewController, let vc = observer() {
|
||||
vc.dismiss(animated: false)
|
||||
}
|
||||
let vc = makePopUpViewController(viewType: viewType, height: height, backgroundColor: backgroundColor)
|
||||
let weakObserver = { [weak vc] in return vc }
|
||||
context.popUpViewController = weakObserver
|
||||
present(viewController: vc)
|
||||
}
|
||||
|
||||
func dismissPopupViewController(completion: (() -> Void)? = nil) {
|
||||
guard let observer = context.popUpViewController, let vc = observer() else {
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
vc.viewModel.changeSearchControllerActive()
|
||||
vc.dismiss(animated: true, completion: completion)
|
||||
context.popUpViewController = nil
|
||||
}
|
||||
|
||||
func pop(animated: Bool = true) {
|
||||
guard let viewControllerArray = navController?.viewControllers else { return }
|
||||
if viewControllerArray.count <= 1 {
|
||||
viewControllerArray.first?.dismiss(animated: true)
|
||||
navController?.dismiss(animated: true)
|
||||
context.rootNavigation = nil
|
||||
} else {
|
||||
if let vc = viewControllerArray.last, vc is ConferenceMainViewController {
|
||||
navController?.popViewController(animated: animated)
|
||||
context.rootNavigation = nil
|
||||
} else {
|
||||
navController?.popViewController(animated: animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func popToRoomEntranceViewController() {
|
||||
if let navController = navController {
|
||||
var controllerArray = navController.viewControllers
|
||||
controllerArray.reverse()
|
||||
for vc in controllerArray {
|
||||
if vc is PopUpViewController {
|
||||
vc.dismiss(animated: true)
|
||||
} else {
|
||||
pop()
|
||||
}
|
||||
if vc is ConferenceMainViewController {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if let vc = context.rootViewController {
|
||||
vc.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
class func presentAlert(title: String?, message: String?, sureTitle:String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?, autoConfirmSeconds: Int? = nil) {
|
||||
var timer: Timer?
|
||||
var remainingSeconds = autoConfirmSeconds ?? 0
|
||||
|
||||
let alertVC = UIAlertController(title: title,
|
||||
message: message,
|
||||
preferredStyle: .alert)
|
||||
if let declineTitle = declineTitle {
|
||||
let declineAction = UIAlertAction(title: declineTitle, style: .destructive) { _ in
|
||||
declineBlock?()
|
||||
}
|
||||
declineAction.setValue(UIColor(0x4F586B), forKey: "titleTextColor")
|
||||
alertVC.addAction(declineAction)
|
||||
}
|
||||
let sureActionTitle = (autoConfirmSeconds != nil) ? "\(sureTitle ?? "") (\(remainingSeconds))" : sureTitle
|
||||
let sureAction = UIAlertAction(title: sureActionTitle, style: .default) { _ in
|
||||
sureBlock?()
|
||||
timer?.invalidate()
|
||||
}
|
||||
alertVC.addAction(sureAction)
|
||||
|
||||
if autoConfirmSeconds != nil {
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
|
||||
remainingSeconds -= 1
|
||||
if remainingSeconds <= 0 {
|
||||
sureBlock?()
|
||||
timer?.invalidate()
|
||||
} else {
|
||||
sureAction.setValue("\(sureTitle ?? "") (\(remainingSeconds))", forKey: "title")
|
||||
}
|
||||
}
|
||||
}
|
||||
shared.getCurrentWindowViewController()?.present(alertVC, animated: true)
|
||||
let weakObserver = { [weak alertVC] in return alertVC }
|
||||
shared.context.alterControllers.append(weakObserver)
|
||||
}
|
||||
|
||||
func dismissAllAlertController(complete: @escaping (()->())) {
|
||||
guard context.alterControllers.count > 0 else {
|
||||
complete()
|
||||
return
|
||||
}
|
||||
dismissAlertController(index: context.alterControllers.count - 1) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.context.alterControllers = []
|
||||
complete()
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissAlertController(index: Int, complete: @escaping (()->())) {
|
||||
if index < 0 {
|
||||
complete()
|
||||
return
|
||||
}
|
||||
if let observer = context.alterControllers[safe: index], let vc = observer() {
|
||||
vc.dismiss(animated: true) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.dismissAlertController(index: index - 1, complete: complete)
|
||||
}
|
||||
} else {
|
||||
dismissAlertController(index: index-1, complete: complete)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class func makeToast(toast: String) {
|
||||
shared.getCurrentWindowViewController()?.view.makeToast(toast)
|
||||
}
|
||||
|
||||
class func makeToastInCenter(toast: String, duration:TimeInterval) {
|
||||
guard let windowView = shared.getCurrentWindowViewController()?.view else {return}
|
||||
windowView.makeToast(toast,duration: duration,position:TUICSToastPositionCenter)
|
||||
}
|
||||
|
||||
class func makeToastInWindow(toast: String, duration:TimeInterval) {
|
||||
guard let window = RoomRouter.getCurrentWindow() else {return}
|
||||
window.makeToast(toast,duration: duration,position:TUICSToastPositionCenter)
|
||||
}
|
||||
|
||||
class func getCurrentWindow() -> UIWindow? {
|
||||
var windows: [UIWindow]
|
||||
if #available(iOS 13.0, *), let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
|
||||
windows = windowScene.windows
|
||||
} else {
|
||||
windows = UIApplication.shared.windows
|
||||
}
|
||||
if let keyWindow = windows.first(where: { $0.isKeyWindow }) {
|
||||
return keyWindow
|
||||
} else {
|
||||
return windows.last(where: { $0.windowLevel == .normal && $0.isHidden == false &&
|
||||
CGRectEqualToRect($0.bounds , UIScreen.main.bounds) })
|
||||
}
|
||||
}
|
||||
|
||||
func initializeNavigationController(rootViewController: UIViewController) {
|
||||
guard context.rootNavigation == nil else { return }
|
||||
if let nav = rootViewController.navigationController {
|
||||
context.rootNavigation = nav
|
||||
}
|
||||
context.rootViewController = rootViewController
|
||||
}
|
||||
|
||||
@objc func handleDeviceOrientationChange() {
|
||||
guard context.currentLandscape != isLandscape else { return }
|
||||
destroyChatWindow()
|
||||
context.currentLandscape = isLandscape
|
||||
}
|
||||
|
||||
deinit {
|
||||
unsubscribeEvent()
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
|
||||
extension RoomRouter {
|
||||
|
||||
func push(viewController: UIViewController, animated: Bool = true) {
|
||||
guard let navController = navController else {
|
||||
createRootNavigationAndPresent(controller: viewController)
|
||||
return
|
||||
}
|
||||
navController.pushViewController(viewController, animated: animated)
|
||||
}
|
||||
|
||||
func present(viewController: UIViewController, animated: Bool = true) {
|
||||
if #available(iOS 13.0, *) {
|
||||
viewController.modalPresentationStyle = .automatic
|
||||
} else {
|
||||
viewController.modalPresentationStyle = .overFullScreen
|
||||
}
|
||||
if let navController = navController {
|
||||
navController.present(viewController, animated: animated)
|
||||
} else if let vc = context.rootViewController {
|
||||
vc.present(viewController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func createRootNavigationAndPresent(controller: UIViewController) {
|
||||
let navigationController = RoomKitNavigationController(rootViewController: controller)
|
||||
navigationController.modalPresentationStyle = .fullScreen
|
||||
context.rootNavigation = navigationController
|
||||
if #available(iOS 13.0, *) {
|
||||
setupNavigationBarAppearance()
|
||||
if let appearance = context.appearance as? UINavigationBarAppearance {
|
||||
navigationController.navigationBar.standardAppearance = appearance
|
||||
navigationController.navigationBar.scrollEdgeAppearance = appearance
|
||||
}
|
||||
} else {
|
||||
navigationController.navigationBar.shadowImage = UIImage()
|
||||
navigationController.navigationBar.barStyle = .default
|
||||
}
|
||||
let weakObserver = { [weak navigationController] in
|
||||
return navigationController
|
||||
}
|
||||
guard let controller = getCurrentWindowViewController() else { return }
|
||||
controller.present(navigationController, animated: true)
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
private func setupNavigationBarAppearance() {
|
||||
guard let barAppearance = context.appearance as? UINavigationBarAppearance else {
|
||||
return
|
||||
}
|
||||
barAppearance.configureWithDefaultBackground()
|
||||
barAppearance.shadowColor = nil
|
||||
barAppearance.backgroundEffect = nil
|
||||
barAppearance.backgroundColor = .white
|
||||
}
|
||||
|
||||
private func getCurrentWindowViewController() -> UIViewController? {
|
||||
var keyWindow: UIWindow?
|
||||
for window in UIApplication.shared.windows {
|
||||
if window.isMember(of: UIWindow.self), window.isKeyWindow {
|
||||
keyWindow = window
|
||||
break
|
||||
}
|
||||
}
|
||||
guard let rootController = keyWindow?.rootViewController else {
|
||||
return nil
|
||||
}
|
||||
func findCurrentController(from vc: UIViewController?) -> UIViewController? {
|
||||
if let nav = vc as? UINavigationController {
|
||||
return findCurrentController(from: nav.topViewController)
|
||||
} else if let tabBar = vc as? UITabBarController {
|
||||
return findCurrentController(from: tabBar.selectedViewController)
|
||||
} else if let presented = vc?.presentedViewController {
|
||||
return findCurrentController(from: presented)
|
||||
}
|
||||
return vc
|
||||
}
|
||||
let viewController = findCurrentController(from: rootController)
|
||||
return viewController
|
||||
}
|
||||
|
||||
private func makeMainViewController() -> UIViewController {
|
||||
let controller = ConferenceMainViewController()
|
||||
return controller
|
||||
}
|
||||
|
||||
private func makePopUpViewController(viewType: PopUpViewType, height: CGFloat, backgroundColor: UIColor) -> PopUpViewController {
|
||||
let controller = PopUpViewController(popUpViewModelFactory: self, viewType: viewType, height: height, backgroundColor: backgroundColor)
|
||||
return controller
|
||||
}
|
||||
|
||||
private func destroyChatWindow() {
|
||||
guard context.chatWindow != nil else { return }
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.context.chatWindow = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RoomRouter.RoomNavigationDelegate: UINavigationControllerDelegate {
|
||||
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
|
||||
if viewController is ConferenceMainViewController {
|
||||
if #available(iOS 13.0, *) {
|
||||
if let appearance = RoomRouter.shared.context.appearance as? UINavigationBarAppearance {
|
||||
navigationController.navigationBar.standardAppearance = appearance
|
||||
navigationController.navigationBar.scrollEdgeAppearance = appearance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RoomRouter: PopUpViewModelFactory {
|
||||
func makeRootViewModel(viewType: PopUpViewType, height: CGFloat, backgroundColor: UIColor) -> PopUpViewModel {
|
||||
let viewModel = PopUpViewModel(viewType: viewType, height: height)
|
||||
viewModel.backgroundColor = backgroundColor
|
||||
return viewModel
|
||||
}
|
||||
}
|
||||
|
||||
extension RoomRouter: RoomKitUIEventResponder {
|
||||
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
|
||||
switch key {
|
||||
case .TUIRoomKitService_ShowRoomVideoFloatView:
|
||||
dismissPopupViewController()
|
||||
popToRoomEntranceViewController()
|
||||
RoomVideoFloatView.show()
|
||||
case .TUIRoomKitService_ShowRoomMainView:
|
||||
RoomVideoFloatView.dismiss()
|
||||
self.pushMainViewController()
|
||||
case .TUIRoomKitService_HiddenChatWindow:
|
||||
destroyChatWindow()
|
||||
case .TUIRoomKitService_DismissConferenceViewController:
|
||||
dismissAllAlertController() { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.dismissPopupViewController() { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.popToRoomEntranceViewController()
|
||||
}
|
||||
}
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var chatText: String {
|
||||
localized("Chat")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// ConferenceMainViewState.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/9/3.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ConferenceMainViewState: Codable {
|
||||
var isInternalCreation = false
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// ConferenceMainViewStore.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/9/3.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
protocol ConferenceMainViewStore {
|
||||
var isInternalCreation: Bool { get }
|
||||
func updateInternalCreation(isInternalCreation: Bool)
|
||||
func dispatch(action: Action)
|
||||
func select<Value: Equatable>(_ selector: Selector<ConferenceMainViewState, Value>) -> AnyPublisher<Value, Never>
|
||||
func selectCurrent<Value>(_ selector: Selector<ConferenceMainViewState, Value>) -> Value
|
||||
}
|
||||
|
||||
class ConferenceMainViewStoreProvider {
|
||||
static let updateInternalCreation = ActionTemplate(id: "updateInternalCreation", payloadType: Bool.self)
|
||||
private(set) lazy var store: Store<ConferenceMainViewState, Void> = Store(initialState: ConferenceMainViewState())
|
||||
private let conferenceMainViewReducer = Reducer<ConferenceMainViewState>(
|
||||
ReduceOn(updateInternalCreation) { state,action in
|
||||
state.isInternalCreation = action.payload
|
||||
}
|
||||
)
|
||||
|
||||
init() {
|
||||
initStore()
|
||||
}
|
||||
|
||||
deinit {
|
||||
store.unregister(reducer: conferenceMainViewReducer)
|
||||
}
|
||||
|
||||
private func initStore() {
|
||||
store.register(reducer: conferenceMainViewReducer)
|
||||
}
|
||||
}
|
||||
|
||||
extension ConferenceMainViewStoreProvider: ConferenceMainViewStore {
|
||||
var isInternalCreation: Bool {
|
||||
return store.state.isInternalCreation
|
||||
}
|
||||
|
||||
func updateInternalCreation(isInternalCreation: Bool) {
|
||||
store.dispatch(action: ConferenceMainViewStoreProvider.updateInternalCreation(payload: isInternalCreation))
|
||||
}
|
||||
|
||||
func dispatch(action: Action) {
|
||||
store.dispatch(action: action)
|
||||
}
|
||||
|
||||
func select<Value>(_ selector: Selector<ConferenceMainViewState, Value>) -> AnyPublisher<Value, Never> where Value : Equatable {
|
||||
return store.select(selector)
|
||||
.removeDuplicates()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func selectCurrent<Value>(_ selector: Selector<ConferenceMainViewState, Value>) -> Value {
|
||||
return store.selectCurrent(selector)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// ConferenceMainViewStoreRegister.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/9/3.
|
||||
//
|
||||
|
||||
import Factory
|
||||
|
||||
extension Container {
|
||||
var conferenceMainViewStore: Factory<ConferenceMainViewStore> {
|
||||
Factory(self) {
|
||||
ConferenceMainViewStoreProvider()
|
||||
}
|
||||
.shared
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
//
|
||||
// BottomItemView.swift
|
||||
// Alamofire
|
||||
//
|
||||
// Created by aby on 2022/12/23.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class BottomItemView: UIView {
|
||||
|
||||
var itemData: ButtonItemData
|
||||
|
||||
var engineManager: EngineManager {
|
||||
EngineManager.shared
|
||||
}
|
||||
|
||||
let button: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
return button
|
||||
}()
|
||||
|
||||
let label: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFont.systemFont(ofSize: 10.0)
|
||||
label.textColor = UIColor(0xD1D9EC)
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
return label
|
||||
}()
|
||||
|
||||
let imageView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
return view
|
||||
}()
|
||||
|
||||
let noticeView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(0xED414D)
|
||||
view.layer.borderWidth = 3
|
||||
view.layer.borderColor = UIColor(0x2A2D38).cgColor
|
||||
view.layer.cornerRadius = 12
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
let noticeLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor(0xFFFFFF)
|
||||
label.textAlignment = .center
|
||||
label.font = UIFont.systemFont(ofSize: 12, weight: .semibold)
|
||||
label.backgroundColor = .clear
|
||||
return label
|
||||
}()
|
||||
|
||||
// MARK: - initialized function
|
||||
init(itemData: ButtonItemData) {
|
||||
self.itemData = itemData
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - view layout
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
self.layer.cornerRadius = 10
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(button)
|
||||
button.addSubview(imageView)
|
||||
button.addSubview(label)
|
||||
button.addSubview(noticeView)
|
||||
noticeView.addSubview(noticeLabel)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
button.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
imageView.snp.makeConstraints { make in
|
||||
if itemData.normalTitle.isEmpty, itemData.selectedTitle.isEmpty {
|
||||
make.centerY.equalToSuperview()
|
||||
} else {
|
||||
make.top.equalToSuperview().offset(7)
|
||||
}
|
||||
make.width.height.equalTo(24)
|
||||
make.centerX.equalToSuperview()
|
||||
}
|
||||
label.snp.makeConstraints { make in
|
||||
if itemData.normalIcon.isEmpty, itemData.selectedIcon.isEmpty {
|
||||
make.centerY.equalToSuperview()
|
||||
} else {
|
||||
make.top.equalTo(imageView.snp.bottom).offset(2)
|
||||
}
|
||||
make.width.equalToSuperview()
|
||||
make.height.equalTo(14)
|
||||
}
|
||||
noticeLabel.snp.makeConstraints { make in
|
||||
make.trailing.equalTo(button).offset(-7)
|
||||
make.top.equalToSuperview().offset(4)
|
||||
make.width.height.greaterThanOrEqualTo(16)
|
||||
}
|
||||
noticeView.snp.makeConstraints { make in
|
||||
make.leading.top.equalTo(noticeLabel).offset(-4)
|
||||
make.trailing.bottom.equalTo(noticeLabel).offset(4)
|
||||
make.width.lessThanOrEqualTo(button)
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
setupViewState(item: itemData)
|
||||
button.addTarget(self, action: #selector(clickMenuButton(sender:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
func setupViewState(item: ButtonItemData) {
|
||||
itemData = item
|
||||
button.isSelected = item.isSelect
|
||||
button.isEnabled = item.isEnabled
|
||||
imageView.image = item.isSelect ? itemData.selectedImage : itemData.normalImage
|
||||
label.text = item.isSelect ? itemData.selectedTitle : itemData.normalTitle
|
||||
button.alpha = item.alpha
|
||||
noticeView.isHidden = !item.hasNotice
|
||||
noticeLabel.text = item.noticeText
|
||||
}
|
||||
|
||||
@objc
|
||||
func clickMenuButton(sender: UIView) {
|
||||
itemData.action?(sender)
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
//
|
||||
// BottomView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by aby on 2022/12/21.
|
||||
// Copyright © 2022 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class BottomView: UIView {
|
||||
// MARK: - store property
|
||||
let viewModel: BottomViewModel
|
||||
private var viewArray: [BottomItemView] = []
|
||||
var isUnfold: Bool = false
|
||||
let unfoldHeight = Float(130.scale375Height())
|
||||
let packUpHeight = Float(68.scale375Height())
|
||||
|
||||
let baseButtonMenuView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.axis = .horizontal
|
||||
view.alignment = .center
|
||||
view.distribution = .equalSpacing
|
||||
view.spacing = 10
|
||||
return view
|
||||
}()
|
||||
|
||||
let moreButtonMenuView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.axis = .horizontal
|
||||
view.alignment = .center
|
||||
view.distribution = .equalSpacing
|
||||
view.spacing = 10
|
||||
return view
|
||||
}()
|
||||
|
||||
let buttonMenuView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(0x0F1014)
|
||||
view.layer.cornerRadius = 12
|
||||
return view
|
||||
}()
|
||||
|
||||
let backgroundView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(0x0F1014)
|
||||
return view
|
||||
}()
|
||||
|
||||
// MARK: - initialized function
|
||||
|
||||
init(viewModel: BottomViewModel) {
|
||||
self.viewModel = viewModel
|
||||
super.init(frame: .zero)
|
||||
backgroundColor = .clear
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - view layout
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(backgroundView)
|
||||
addSubview(buttonMenuView)
|
||||
buttonMenuView.addSubview(moreButtonMenuView)
|
||||
buttonMenuView.addSubview(baseButtonMenuView)
|
||||
moreButtonMenuView.isHidden = true
|
||||
setupMenuStackView(items: viewModel.viewItems)
|
||||
layoutMoreButtonMenu()
|
||||
}
|
||||
|
||||
func setupMenuStackView(items: [ButtonItemData]) {
|
||||
for i in 0...(items.count - 1) {
|
||||
guard let item = viewModel.viewItems[safe: i] else { continue }
|
||||
let view = BottomItemView(itemData: item)
|
||||
let size = item.size ?? CGSize(width: 52.scale375(), height: 52.scale375())
|
||||
view.snp.makeConstraints { make in
|
||||
make.height.equalTo(size.height)
|
||||
make.width.equalTo(size.width)
|
||||
}
|
||||
view.backgroundColor = item.backgroundColor ?? UIColor(0x2A2D38)
|
||||
viewArray.append(view)
|
||||
if i < 6 {
|
||||
baseButtonMenuView.addArrangedSubview(view)
|
||||
} else {
|
||||
moreButtonMenuView.addArrangedSubview(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func layoutMoreButtonMenu() {
|
||||
let emptyViewCount = baseButtonMenuView.subviews.count - moreButtonMenuView.subviews.count
|
||||
if emptyViewCount <= 0 {return}
|
||||
for _ in 1...emptyViewCount {
|
||||
let emptyView = BottomItemView(itemData: ButtonItemData())
|
||||
emptyView.snp.makeConstraints { make in
|
||||
make.height.equalTo(52.scale375())
|
||||
make.width.equalTo(52.scale375())
|
||||
moreButtonMenuView.addArrangedSubview(emptyView)
|
||||
}
|
||||
viewArray.append(emptyView)
|
||||
}
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
backgroundView.snp.makeConstraints { make in
|
||||
make.bottom.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(packUpHeight)
|
||||
}
|
||||
let width = min(kScreenWidth, kScreenHeight)
|
||||
buttonMenuView.snp.makeConstraints { make in
|
||||
make.width.equalTo(width)
|
||||
make.bottom.centerX.height.equalToSuperview()
|
||||
}
|
||||
baseButtonMenuView.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(8.scale375())
|
||||
make.height.equalTo(52.scale375())
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
}
|
||||
moreButtonMenuView.snp.makeConstraints { make in
|
||||
make.bottom.equalToSuperview()
|
||||
make.height.equalTo(52.scale375())
|
||||
make.leading.trailing.equalTo(baseButtonMenuView)
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
viewModel.viewResponder = self
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
|
||||
extension BottomView: BottomViewModelResponder {
|
||||
func updateButtonView(item: ButtonItemData) {
|
||||
guard let view = viewArray.first(where: { $0.itemData.buttonType == item.buttonType }) else { return }
|
||||
view.setupViewState(item: item)
|
||||
}
|
||||
|
||||
func showAlert(title: String?, message: String?, sureTitle: String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?) {
|
||||
RoomRouter.presentAlert(title: title, message: message, sureTitle: sureTitle, declineTitle: declineTitle, sureBlock: sureBlock, declineBlock: declineBlock)
|
||||
}
|
||||
|
||||
func updateStackView(items: [ButtonItemData]) {
|
||||
viewArray.forEach { view in
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
viewArray = []
|
||||
setupMenuStackView(items: items)
|
||||
layoutMoreButtonMenu()
|
||||
}
|
||||
|
||||
func makeToast(text: String) {
|
||||
RoomRouter.makeToastInCenter(toast: text, duration: 1)
|
||||
}
|
||||
|
||||
private func updateBottomViewConstraints(isUnfold: Bool, completion: @escaping () -> Void) {
|
||||
UIView.animate(withDuration: 0.3) { [weak self] () in
|
||||
guard let self = self else { return }
|
||||
self.snp.updateConstraints { make in
|
||||
make.height.equalTo(isUnfold ? self.unfoldHeight : self.packUpHeight)
|
||||
}
|
||||
self.superview?.layoutIfNeeded()
|
||||
} completion: { _ in
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
func updataBottomView(isUp: Bool) {
|
||||
buttonMenuView.backgroundColor = isUp ? UIColor(0x2A2D38) : UIColor(0x0F1014)
|
||||
self.isUnfold = isUp
|
||||
if isUp {
|
||||
updateBottomViewConstraints(isUnfold: true) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.moreButtonMenuView.isHidden = false
|
||||
}
|
||||
} else {
|
||||
moreButtonMenuView.isHidden = true
|
||||
updateBottomViewConstraints(isUnfold: false) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var leaveRoomTitle: String {
|
||||
localized("Are you sure you want to leave the conference?")
|
||||
}
|
||||
|
||||
static var destroyRoomTitle: String {
|
||||
localized("Are you sure you want to end the conference?")
|
||||
}
|
||||
|
||||
static var destroyRoomCancelTitle: String {
|
||||
localized("Wait")
|
||||
}
|
||||
|
||||
static var logoutOkText: String {
|
||||
localized("OK")
|
||||
}
|
||||
|
||||
static var dismissMeetingTitle: String {
|
||||
localized("If you don't want to end the conference")
|
||||
}
|
||||
|
||||
static var appointNewHostText: String {
|
||||
localized("Please appoint a new host before leaving the conference")
|
||||
}
|
||||
|
||||
static var leaveMeetingText: String {
|
||||
localized("Leave conference")
|
||||
}
|
||||
|
||||
static var dismissMeetingText: String {
|
||||
localized("End conference")
|
||||
}
|
||||
|
||||
static var cancelText: String {
|
||||
localized("Cancel")
|
||||
}
|
||||
|
||||
static var toastTitleText: String {
|
||||
localized("Share Screen")
|
||||
}
|
||||
static var toastMessageText: String {
|
||||
localized("Stop TUIRoom screen sharing screen live?")
|
||||
}
|
||||
static var toastStopText: String {
|
||||
localized("Stop")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
//
|
||||
// ConferencePasswordView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/7/30.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Factory
|
||||
import RTCRoomEngine
|
||||
|
||||
class ConferencePasswordView: UIView {
|
||||
var roomId: String?
|
||||
private let maxNumber = 6
|
||||
weak var viewModel: ConferenceMainViewModel?
|
||||
|
||||
let shieldingView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(0x0F1014).withAlphaComponent(0.7)
|
||||
return view
|
||||
}()
|
||||
|
||||
let contentView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(0xFFFFFF)
|
||||
view.layer.cornerRadius = 10
|
||||
return view
|
||||
}()
|
||||
|
||||
let titleLabel: UILabel = {
|
||||
let view = UILabel()
|
||||
view.text = .conferencePassword
|
||||
view.backgroundColor = .clear
|
||||
view.textColor = UIColor(0x0F1014)
|
||||
view.font = UIFont.systemFont(ofSize: 16, weight: .medium)
|
||||
view.textAlignment = .center
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var textField: UITextField = {
|
||||
let view = UITextField(frame: .zero)
|
||||
view.backgroundColor = .clear
|
||||
view.placeholder = .pleaseEnterTheConferencePassword
|
||||
view.textColor = UIColor(0x2B2E38)
|
||||
view.tintColor = UIColor(0x2B2E38).withAlphaComponent(0.7)
|
||||
view.font = UIFont.systemFont(ofSize: 16, weight: .regular)
|
||||
view.keyboardType = .numberPad
|
||||
view.textAlignment = isRTL ? .right : .left
|
||||
view.layer.cornerRadius = 10
|
||||
view.layer.borderWidth = 1
|
||||
view.layer.borderColor = UIColor(0x1C66E5).cgColor
|
||||
view.delegate = self
|
||||
view.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
|
||||
view.leftViewMode = .always
|
||||
let deleteButton = UIButton(type: .system)
|
||||
deleteButton.frame = CGRect(x: 0, y: 0, width: 60, height: 30)
|
||||
deleteButton.contentEdgeInsets = UIEdgeInsets(top: 5, left: 20, bottom: 5, right: 20)
|
||||
deleteButton.setImage(UIImage(named: "room_cancel", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
|
||||
deleteButton.addTarget(self, action: #selector(deleteAction(sender:)), for: .touchUpInside)
|
||||
view.rightView = deleteButton
|
||||
view.rightViewMode = .whileEditing
|
||||
return view
|
||||
}()
|
||||
|
||||
let cancelButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setTitle(.cancel, for: .normal)
|
||||
button.setTitleColor(UIColor(0x4F586B), for: .normal)
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .regular)
|
||||
button.layer.borderWidth = 0.5
|
||||
button.layer.borderColor = UIColor(0xD5E0F2).withAlphaComponent(0.5).cgColor
|
||||
return button
|
||||
}()
|
||||
|
||||
let sureButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setTitle(.join, for: .normal)
|
||||
button.setTitleColor(UIColor(0x1C66E5), for: .normal)
|
||||
button.setTitleColor(UIColor(0x1C66E5).withAlphaComponent(0.5), for: .disabled)
|
||||
button.layer.borderWidth = 0.5
|
||||
button.layer.borderColor = UIColor(0xD5E0F2).withAlphaComponent(0.5).cgColor
|
||||
return button
|
||||
}()
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
isViewReady = true
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
addSubview(shieldingView)
|
||||
addSubview(contentView)
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(textField)
|
||||
contentView.addSubview(cancelButton)
|
||||
contentView.addSubview(sureButton)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
shieldingView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
contentView.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.width.equalTo(323.scale375())
|
||||
make.height.equalTo(180.scale375Height())
|
||||
}
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(24.scale375Height())
|
||||
make.centerX.equalToSuperview()
|
||||
}
|
||||
textField.snp.makeConstraints { make in
|
||||
make.height.equalTo(40.scale375Height())
|
||||
make.width.equalTo(298.scale375())
|
||||
make.centerX.equalToSuperview()
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(14.scale375Height())
|
||||
}
|
||||
cancelButton.snp.makeConstraints { make in
|
||||
make.width.equalToSuperview().multipliedBy(0.5)
|
||||
make.leading.equalToSuperview()
|
||||
make.height.equalTo(54.scale375Height())
|
||||
make.bottom.equalToSuperview()
|
||||
}
|
||||
sureButton.snp.makeConstraints { make in
|
||||
make.width.equalToSuperview().multipliedBy(0.5)
|
||||
make.trailing.equalToSuperview()
|
||||
make.height.equalTo(54.scale375Height())
|
||||
make.bottom.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
private func bindInteraction() {
|
||||
cancelButton.addTarget(self, action: #selector(cancelAction(sender:)), for: .touchUpInside)
|
||||
sureButton.addTarget(self, action: #selector(sureAction(sender:)), for: .touchUpInside)
|
||||
updateSureButton()
|
||||
}
|
||||
|
||||
private func updateSureButton() {
|
||||
guard let text = textField.text else { return }
|
||||
sureButton.isEnabled = text.count > 0
|
||||
}
|
||||
|
||||
@objc func cancelAction(sender: UIButton) {
|
||||
guard superview != nil else { return }
|
||||
removeFromSuperview()
|
||||
guard let roomId = roomId else { return }
|
||||
viewModel?.handleWrongPasswordFault(roomId: roomId)
|
||||
}
|
||||
|
||||
@objc func sureAction(sender: UIButton) {
|
||||
guard superview != nil else { return }
|
||||
viewModel?.joinConferenceParams?.password = textField.text
|
||||
viewModel?.joinConference()
|
||||
}
|
||||
|
||||
func hide() {
|
||||
self.isHidden = true
|
||||
textField.resignFirstResponder()
|
||||
}
|
||||
|
||||
func show(roomId: String) {
|
||||
self.roomId = roomId
|
||||
self.isHidden = false
|
||||
}
|
||||
|
||||
@objc func deleteAction(sender: UIButton) {
|
||||
textField.text = ""
|
||||
sureButton.isEnabled = false
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
guard let touch = touches.first else { return }
|
||||
let point = touch.location(in: self)
|
||||
guard layer.contains(point) else { return }
|
||||
textField.resignFirstResponder()
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit:\(self)")
|
||||
}
|
||||
|
||||
@Injected(\.conferenceStore) private var store
|
||||
}
|
||||
|
||||
extension ConferencePasswordView: UITextFieldDelegate {
|
||||
func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
textField.isSecureTextEntry = true
|
||||
}
|
||||
|
||||
func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
textField.isSecureTextEntry = false
|
||||
}
|
||||
|
||||
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
guard let text = textField.text else { return true }
|
||||
let newText = NSString(string: text).replacingCharacters(in: range, with: string)
|
||||
sureButton.isEnabled = newText.count > 0
|
||||
return newText.count <= maxNumber
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static let conferencePassword = localized("Conference password")
|
||||
static let join = localized("Join")
|
||||
static let cancel = localized("Cancel")
|
||||
static let pleaseEnterTheConferencePassword = localized("Please enter your room password")
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
//
|
||||
// ExitRoomView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by krabyu on 2023/8/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RTCRoomEngine
|
||||
|
||||
class ExitRoomView: UIView {
|
||||
private let viewModel: ExitRoomViewModel
|
||||
private var isViewReady: Bool = false
|
||||
var currentUser: UserEntity {
|
||||
EngineManager.shared.store.currentUser
|
||||
}
|
||||
var roomInfo: TUIRoomInfo {
|
||||
EngineManager.shared.store.roomInfo
|
||||
}
|
||||
|
||||
let panelControl : UIControl = {
|
||||
let control = UIControl()
|
||||
control.backgroundColor = .clear
|
||||
return control
|
||||
}()
|
||||
|
||||
let contentView: UIView = {
|
||||
let view = UIView(frame: .zero)
|
||||
view.backgroundColor = UIColor(0x17181F)
|
||||
view.layer.cornerRadius = 12
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor(0x7C85A6)
|
||||
label.font = UIFont(name: "PingFangSC-Regular", size: 12)
|
||||
label.textAlignment = .center
|
||||
label.numberOfLines = 0
|
||||
label.lineBreakMode = .byWordWrapping
|
||||
label.text = viewModel.isShownDestroyRoomButton() && viewModel.isShownLeaveRoomButton() ? .appointOwnerText : .leaveRoomTipText
|
||||
return label
|
||||
}()
|
||||
|
||||
let boundary1View: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(0x4F586B,alpha: 0.3)
|
||||
return view
|
||||
}()
|
||||
|
||||
let leaveRoomButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.setTitle(.leaveRoomText, for: .normal)
|
||||
button.titleLabel?.font = UIFont(name: "PingFangSC-Regular", size: 18)
|
||||
button.setTitleColor(UIColor(0x006CFF), for: .normal)
|
||||
button.backgroundColor = UIColor(0x17181F)
|
||||
button.isEnabled = true
|
||||
return button
|
||||
}()
|
||||
|
||||
let boundary2View: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(0x4F586B,alpha: 0.3)
|
||||
return view
|
||||
}()
|
||||
|
||||
let destroyRoomButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.setTitle(.exitRoomText, for: .normal)
|
||||
button.titleLabel?.font = UIFont(name: "PingFangSC-Regular", size: 18)
|
||||
button.setTitleColor(UIColor(0xE5395C), for: .normal)
|
||||
button.backgroundColor = UIColor(0x17181F)
|
||||
button.isEnabled = true
|
||||
return button
|
||||
}()
|
||||
|
||||
init(viewModel: ExitRoomViewModel) {
|
||||
self.viewModel = viewModel
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(panelControl)
|
||||
addSubview(contentView)
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(boundary1View)
|
||||
contentView.addSubview(leaveRoomButton)
|
||||
contentView.addSubview(boundary2View)
|
||||
contentView.addSubview(destroyRoomButton)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
let titleLabelHeight = 67.scale375Height()
|
||||
let leaveRoomButtonHeight = viewModel.isShownLeaveRoomButton() ? 57.scale375Height() : 0
|
||||
let destroyRoomButtonHeight = currentUser.userId == roomInfo.ownerId ? 57.scale375Height() : 0
|
||||
let space = 20.scale375Height()
|
||||
let contentViewHeight = titleLabelHeight + leaveRoomButtonHeight + destroyRoomButtonHeight + space
|
||||
panelControl.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
contentView.snp.makeConstraints { make in
|
||||
make.height.equalTo(contentViewHeight)
|
||||
make.trailing.equalTo(safeAreaLayoutGuide.snp.trailing)
|
||||
make.leading.equalTo(safeAreaLayoutGuide.snp.leading)
|
||||
make.bottom.equalToSuperview()
|
||||
}
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(titleLabelHeight)
|
||||
}
|
||||
boundary1View.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom)
|
||||
make.height.equalTo(1.scale375Height())
|
||||
make.leading.trailing.equalToSuperview()
|
||||
}
|
||||
leaveRoomButton.snp.makeConstraints { make in
|
||||
make.top.equalTo(boundary1View.snp.bottom)
|
||||
make.height.equalTo(leaveRoomButtonHeight)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
}
|
||||
boundary2View.snp.makeConstraints { make in
|
||||
make.top.equalTo(leaveRoomButton.snp.bottom)
|
||||
make.height.equalTo(1.scale375Height())
|
||||
make.leading.trailing.equalToSuperview()
|
||||
}
|
||||
destroyRoomButton.snp.makeConstraints { make in
|
||||
make.top.equalTo(boundary2View.snp.bottom)
|
||||
make.height.equalTo(destroyRoomButtonHeight)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
setupViewState()
|
||||
viewModel.viewResponder = self
|
||||
leaveRoomButton.addTarget(self, action: #selector(leaveRoomAction), for: .touchUpInside)
|
||||
destroyRoomButton.addTarget(self, action: #selector(destroyRoomAction), for: .touchUpInside)
|
||||
contentView.transform = CGAffineTransform(translationX: 0, y: kScreenHeight)
|
||||
panelControl.addTarget(self, action: #selector(clickBackgroundView), for: .touchUpInside)
|
||||
}
|
||||
|
||||
private func setupViewState() {
|
||||
destroyRoomButton.isHidden = !viewModel.isShownDestroyRoomButton()
|
||||
leaveRoomButton.isHidden = !viewModel.isShownLeaveRoomButton()
|
||||
boundary2View.isHidden = !viewModel.isShownDestroyRoomButton() || !viewModel.isShownLeaveRoomButton()
|
||||
}
|
||||
|
||||
@objc func clickBackgroundView() {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@objc func leaveRoomAction(sender: UIView) {
|
||||
viewModel.leaveRoomAction()
|
||||
}
|
||||
|
||||
@objc func destroyRoomAction(sender: UIView) {
|
||||
viewModel.destroyRoom()
|
||||
}
|
||||
|
||||
func show(rootView: UIView) {
|
||||
rootView.addSubview(self)
|
||||
self.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
UIView.animate(withDuration: 0.3) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.alpha = 1
|
||||
self.contentView.transform = .identity
|
||||
}
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
UIView.animate(withDuration: 0.3) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.alpha = 0
|
||||
self.contentView.transform = CGAffineTransform(translationX: 0, y: kScreenHeight)
|
||||
} completion: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
|
||||
extension ExitRoomView: ExitRoomViewModelResponder {
|
||||
func makeToast(message: String) {
|
||||
makeToast(message)
|
||||
}
|
||||
|
||||
func dismissView() {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var leaveRoomTipText: String {
|
||||
localized("Are you sure you want to leave the conference" )
|
||||
}
|
||||
static var appointOwnerText: String {
|
||||
localized("If you do not want to end the conference, please appoint a new moderator before leaving the conference." )
|
||||
}
|
||||
static var leaveRoomText: String {
|
||||
localized("Leave Conference")
|
||||
}
|
||||
static var exitRoomText: String {
|
||||
localized("End Conference")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
//
|
||||
// InviteView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by jeremiawang on 2024/8/26.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Factory
|
||||
|
||||
class InviteView: UIView {
|
||||
private var isViewReady: Bool = false
|
||||
|
||||
let stackView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.axis = .vertical
|
||||
view.alignment = .center
|
||||
view.distribution = .fillEqually
|
||||
view.spacing = 0
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var addUserView: ButtonItemView = {
|
||||
let item = ButtonItemData()
|
||||
item.normalTitle = .addUserText
|
||||
item.normalIcon = "room_add_user"
|
||||
item.resourceBundle = tuiRoomKitBundle()
|
||||
item.buttonType = .muteAudioItemType
|
||||
item.hasLineView = true
|
||||
return ButtonItemView(itemData: item)
|
||||
}()
|
||||
|
||||
private lazy var inviteToJoinView: ButtonItemView = {
|
||||
let item = ButtonItemData()
|
||||
item.normalTitle = .shareRoomText
|
||||
item.normalIcon = "room_invite_to_join"
|
||||
item.resourceBundle = tuiRoomKitBundle()
|
||||
item.buttonType = .muteVideoItemType
|
||||
item.hasLineView = true
|
||||
return ButtonItemView(itemData: item)
|
||||
}()
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
super.draw(rect)
|
||||
self.layer.cornerRadius = 16
|
||||
}
|
||||
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
addSubview(stackView)
|
||||
stackView.addArrangedSubview(addUserView)
|
||||
stackView.addArrangedSubview(inviteToJoinView)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
stackView.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview()
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
make.bottom.equalToSuperview().offset(-34.scale375Height())
|
||||
}
|
||||
addUserView.snp.makeConstraints { make in
|
||||
make.height.equalTo(53.scale375())
|
||||
make.width.equalToSuperview()
|
||||
}
|
||||
inviteToJoinView.snp.makeConstraints { make in
|
||||
make.height.equalTo(53.scale375())
|
||||
make.width.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
addUserView.itemData.action = { sender in
|
||||
self.conferenceStore.dispatch(action: InvitationViewActions.showInvitationPopupView())
|
||||
RoomRouter.shared.dismissPopupViewController()
|
||||
}
|
||||
inviteToJoinView.itemData.action = { sender in
|
||||
RoomRouter.shared.presentPopUpViewController(viewType: .inviteMemberViewType, height: 290.scale375Height())
|
||||
}
|
||||
}
|
||||
|
||||
@Injected(\.navigation) private var route
|
||||
@Injected(\.conferenceStore) var conferenceStore: ConferenceStore
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var addUserText: String {
|
||||
localized("Add user")
|
||||
}
|
||||
static var shareRoomText: String {
|
||||
localized("Share room")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
//
|
||||
// MemberInviteView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by krabyu on 2023/8/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class MemberInviteView: UIView {
|
||||
let viewModel: MemberInviteViewModel
|
||||
private var isViewReady: Bool = false
|
||||
var viewArray: [UIView] = []
|
||||
|
||||
let headView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(0x1B1E26)
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = viewModel.title
|
||||
label.textColor = UIColor(0xD5E0F2)
|
||||
label.font = UIFont(name: "PingFangSC-Regular", size: 18)
|
||||
label.textAlignment = .left
|
||||
return label
|
||||
}()
|
||||
|
||||
let stackView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.axis = .vertical
|
||||
view.alignment = .center
|
||||
view.distribution = .equalSpacing
|
||||
view.spacing = 3
|
||||
view.backgroundColor = UIColor(0x1B1E26)
|
||||
return view
|
||||
}()
|
||||
|
||||
let copyButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setTitle(.copyRoomInformation, for: .normal)
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .regular)
|
||||
button.setTitleColor(UIColor(0xB2BBD1), for: .normal)
|
||||
button.titleLabel?.textAlignment = .center
|
||||
button.backgroundColor = UIColor(0x4F586B).withAlphaComponent(0.3)
|
||||
button.layer.cornerRadius = 6
|
||||
return button
|
||||
}()
|
||||
|
||||
init(viewModel: MemberInviteViewModel) {
|
||||
self.viewModel = viewModel
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
super.draw(rect)
|
||||
self.layer.cornerRadius = 12
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(stackView)
|
||||
addSubview(headView)
|
||||
headView.addSubview(titleLabel)
|
||||
addSubview(copyButton)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
headView.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(20.scale375())
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
make.height.equalTo(25.scale375())
|
||||
}
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview()
|
||||
make.leading.equalToSuperview()
|
||||
make.height.equalTo(25.scale375())
|
||||
make.width.equalTo(182.scale375())
|
||||
}
|
||||
stackView.snp.makeConstraints { make in
|
||||
make.top.equalTo(headView.snp.bottom).offset(20.scale375())
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
}
|
||||
copyButton.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
make.height.equalTo(40.scale375Height())
|
||||
make.top.equalTo(stackView.snp.bottom).offset(20.scale375Height())
|
||||
}
|
||||
|
||||
for item in viewModel.messageItems {
|
||||
let view = ListCellItemView(itemData: item)
|
||||
viewArray.append(view)
|
||||
stackView.addArrangedSubview(view)
|
||||
view.snp.makeConstraints { make in
|
||||
make.height.equalTo(24.scale375Height())
|
||||
make.width.equalToSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
backgroundColor = UIColor(0x1B1E26)
|
||||
viewModel.viewResponder = self
|
||||
copyButton.addTarget(self, action: #selector(copyAction(sender:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc func copyAction(sender: UIButton) {
|
||||
viewModel.copyAction()
|
||||
makeToast(.roomInformationCopiedSuccessfully)
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
|
||||
extension MemberInviteView: MemberInviteResponder {
|
||||
func showCopyToast(copyType: CopyType?) {
|
||||
guard let copyType = copyType else { return }
|
||||
var test: String
|
||||
switch copyType {
|
||||
case .copyRoomPassword:
|
||||
test = .copyRoomPasswordSuccess
|
||||
case .copyRoomIdType:
|
||||
test = .copyRoomIdSuccess
|
||||
case .copyRoomLinkType:
|
||||
test = .copyRoomLinkSuccess
|
||||
}
|
||||
RoomRouter.makeToastInCenter(toast: test,duration: 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var copyRoomIdSuccess: String {
|
||||
localized("Conference ID copied.")
|
||||
}
|
||||
static var copyRoomLinkSuccess: String {
|
||||
localized("Conference Link copied.")
|
||||
}
|
||||
static let conferencePasswordSuccess = localized("Conference password copied successfully.")
|
||||
static let copyRoomInformation = localized("Copy room information")
|
||||
static let roomInformationCopiedSuccessfully = localized("Room information copied successfully")
|
||||
static let copyRoomPasswordSuccess = localized("Conference password copied")
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
//
|
||||
// RaiseHandNoticeView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2023/4/7.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class RaiseHandNoticeView: UIView {
|
||||
let imageView: UIImageView = {
|
||||
let image = UIImage(named: "room_raiseHand_notice", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
let imageView = UIImageView(image: image)
|
||||
return imageView
|
||||
}()
|
||||
let dismissButton: UIButton = {
|
||||
let button = UIButton()
|
||||
let image = UIImage(named: "room_raiseHand_dismiss", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
button.setImage(image, for: .normal)
|
||||
return button
|
||||
}()
|
||||
let label: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = .raiseHandNotice
|
||||
label.textColor = .white
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
return label
|
||||
}()
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
addSubview(imageView)
|
||||
addSubview(label)
|
||||
addSubview(dismissButton)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
imageView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
label.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview().offset(-2)
|
||||
make.leading.equalToSuperview().offset(12)
|
||||
make.width.equalTo(250)
|
||||
make.height.equalTo(20)
|
||||
}
|
||||
dismissButton.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview().offset(-2)
|
||||
make.trailing.equalToSuperview().offset(-12)
|
||||
make.width.height.equalTo(20)
|
||||
}
|
||||
}
|
||||
|
||||
private func bindInteraction() {
|
||||
dismissButton.addTarget(self, action: #selector(dismiss), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc func dismiss() {
|
||||
isHidden = true
|
||||
EngineManager.shared.changeRaiseHandNoticeState(isShown: false)
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var raiseHandNotice: String {
|
||||
localized("Raise your hand to speak")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
//
|
||||
// IntroduceRoomView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2023/1/3.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class RoomInfoView: UIView {
|
||||
let viewModel: RoomInfoViewModel
|
||||
private var isViewReady: Bool = false
|
||||
var viewArray: [UIView] = []
|
||||
|
||||
let headView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(0x1B1E26)
|
||||
return view
|
||||
}()
|
||||
|
||||
let nameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor(0xD5E0F2)
|
||||
label.font = UIFont(name: "PingFangSC-Regular", size: 18)
|
||||
label.textAlignment = isRTL ? .right : .left
|
||||
return label
|
||||
}()
|
||||
|
||||
let codeButton: UIButton = {
|
||||
let button = UIButton()
|
||||
let image = UIImage(named: "room_message_code", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
button.setImage(image, for: .normal)
|
||||
button.isHidden = true
|
||||
return button
|
||||
}()
|
||||
|
||||
let stackView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.axis = .vertical
|
||||
view.alignment = .center
|
||||
view.distribution = .equalSpacing
|
||||
view.spacing = 3
|
||||
view.backgroundColor = UIColor(0x1B1E26)
|
||||
return view
|
||||
}()
|
||||
|
||||
let copyButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setTitle(.copyRoomInformation, for: .normal)
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .regular)
|
||||
button.setTitleColor(UIColor(0xB2BBD1), for: .normal)
|
||||
button.titleLabel?.textAlignment = .center
|
||||
button.backgroundColor = UIColor(0x4F586B).withAlphaComponent(0.3)
|
||||
button.layer.cornerRadius = 6
|
||||
return button
|
||||
}()
|
||||
|
||||
init(viewModel: RoomInfoViewModel) {
|
||||
self.viewModel = viewModel
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
backgroundColor = UIColor(0x1B1E26)
|
||||
guard !isViewReady else { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
super.draw(rect)
|
||||
self.layer.cornerRadius = 12
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(stackView)
|
||||
addSubview(headView)
|
||||
headView.addSubview(nameLabel)
|
||||
headView.addSubview(codeButton)
|
||||
addSubview(copyButton)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
stackView.snp.makeConstraints { make in
|
||||
make.top.equalTo(headView.snp.bottom).offset(20.scale375())
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
}
|
||||
headView.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(20.scale375Height())
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
make.height.equalTo(25.scale375())
|
||||
}
|
||||
codeButton.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview()
|
||||
make.trailing.equalToSuperview()
|
||||
make.height.equalTo(25.scale375())
|
||||
make.width.equalTo(68.scale375())
|
||||
}
|
||||
nameLabel.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview()
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(25.scale375())
|
||||
}
|
||||
copyButton.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
make.height.equalTo(40.scale375Height())
|
||||
make.top.equalTo(stackView.snp.bottom).offset(20.scale375Height())
|
||||
}
|
||||
for item in viewModel.messageItems {
|
||||
let view = ListCellItemView(itemData: item)
|
||||
viewArray.append(view)
|
||||
stackView.addArrangedSubview(view)
|
||||
view.snp.makeConstraints { make in
|
||||
make.height.equalTo(24.scale375Height())
|
||||
make.width.equalToSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
backgroundColor = UIColor(0x1B1E26)
|
||||
setupViewState(item: viewModel)
|
||||
codeButton.addTarget(self, action: #selector(codeAction(sender:)), for: .touchUpInside)
|
||||
copyButton.addTarget(self, action: #selector(copyAction(sender: )), for: .touchUpInside)
|
||||
viewModel.viewResponder = self
|
||||
}
|
||||
|
||||
func setupViewState(item: RoomInfoViewModel) {
|
||||
nameLabel.text = viewModel.title
|
||||
}
|
||||
|
||||
@objc func codeAction(sender: UIButton) {
|
||||
viewModel.codeAction(sender: sender)
|
||||
}
|
||||
|
||||
@objc func copyAction(sender: UIButton) {
|
||||
viewModel.copyConferenceDetails()
|
||||
makeToast(.roomInformationCopiedSuccessfully)
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
|
||||
extension RoomInfoView: RoomInfoResponder {
|
||||
func updateNameLabel(_ text: String) {
|
||||
nameLabel.text = text
|
||||
}
|
||||
|
||||
func showCopyToast(copyType: CopyType?) {
|
||||
var test: String
|
||||
guard let copyType = copyType else { return }
|
||||
switch copyType {
|
||||
case .copyRoomPassword:
|
||||
test = .copyRoomPasswordSuccess
|
||||
case .copyRoomIdType:
|
||||
test = .copyRoomIdSuccess
|
||||
case .copyRoomLinkType:
|
||||
test = .copyRoomLinkSuccess
|
||||
}
|
||||
RoomRouter.makeToastInCenter(toast: test,duration: 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var copyRoomIdSuccess: String {
|
||||
localized("Conference ID copied.")
|
||||
}
|
||||
static var copyRoomLinkSuccess: String {
|
||||
localized("Conference Link copied.")
|
||||
}
|
||||
static let conferencePasswordSuccess = localized("Conference password copied successfully.")
|
||||
static let copyRoomInformation = localized("Copy room information")
|
||||
static let roomInformationCopiedSuccessfully = localized("Room information copied successfully")
|
||||
static let copyRoomPasswordSuccess = localized("Conference password copied")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// RoomUserStatusView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2023/7/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class RoomUserStatusView: UIView {
|
||||
private var isOwner: Bool = false
|
||||
private var isViewReady: Bool = false
|
||||
private let homeOwnerImageView: UIImageView = {
|
||||
let imageView = UIImageView(image: UIImage(named: "room_homeowner", in: tuiRoomKitBundle(), compatibleWith: nil))
|
||||
imageView.layer.cornerRadius = 12
|
||||
imageView.layer.masksToBounds = true
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private let userNameLabel: UILabel = {
|
||||
let user = UILabel()
|
||||
user.textColor = .white
|
||||
user.backgroundColor = UIColor.clear
|
||||
user.textAlignment = isRTL ? .right : .left
|
||||
user.numberOfLines = 1
|
||||
user.font = UIFont(name: "PingFangSC-Regular", size: 12)
|
||||
return user
|
||||
}()
|
||||
|
||||
private let voiceVolumeImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
return imageView
|
||||
}()
|
||||
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
isViewReady = true
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
backgroundColor = UIColor(0x22262E, alpha: 0.8)
|
||||
layer.cornerRadius = 12
|
||||
layer.masksToBounds = true
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
addSubview(homeOwnerImageView)
|
||||
addSubview(voiceVolumeImageView)
|
||||
addSubview(userNameLabel)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
updateViewConstraints()
|
||||
userNameLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(voiceVolumeImageView.snp.trailing).offset(5)
|
||||
make.centerY.equalToSuperview()
|
||||
make.trailing.equalToSuperview().offset(-8)
|
||||
}
|
||||
voiceVolumeImageView.snp.makeConstraints { make in
|
||||
make.leading.equalTo(homeOwnerImageView.snp.trailing).offset(6.scale375())
|
||||
make.width.height.equalTo(14)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateViewConstraints() {
|
||||
guard homeOwnerImageView.superview != nil else { return }
|
||||
homeOwnerImageView.snp.remakeConstraints { make in
|
||||
make.leading.equalToSuperview()
|
||||
make.height.equalTo(24)
|
||||
make.top.bottom.equalToSuperview()
|
||||
make.width.equalTo(isOwner ? 24 : 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RoomUserStatusView {
|
||||
func updateUserStatus(userModel: UserEntity) {
|
||||
if !userModel.userName.isEmpty {
|
||||
userNameLabel.text = userModel.userName
|
||||
} else {
|
||||
userNameLabel.text = userModel.userId
|
||||
}
|
||||
isOwner = userModel.userId == EngineManager.shared.store.roomInfo.ownerId
|
||||
updateViewConstraints()
|
||||
updateUserVolume(hasAudio: userModel.hasAudioStream, volume: userModel.userVoiceVolume)
|
||||
}
|
||||
|
||||
func updateUserVolume(hasAudio: Bool, volume: Int) {
|
||||
if !hasAudio {
|
||||
voiceVolumeImageView.image = UIImage(named: "room_mute_audio", in: tuiRoomKitBundle(), compatibleWith: nil)?.checkOverturn()
|
||||
} else {
|
||||
let volumeImageName = volume <= 0 ? "room_voice_volume1" : "room_voice_volume2"
|
||||
voiceVolumeImageView.image = UIImage(named: volumeImageName, in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
//
|
||||
// RoomVideoFloatView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2023/7/11.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Factory
|
||||
|
||||
class RoomVideoFloatView: UIView {
|
||||
@Injected(\.floatChatService) private var store: FloatChatStoreProvider
|
||||
@Injected(\.conferenceStore) private var conferenceStore: ConferenceStore
|
||||
private var isDraging: Bool = false
|
||||
private let viewModel: RoomVideoFloatViewModel
|
||||
private let space: CGFloat = 10
|
||||
private let renderView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(0x5C5C5C)
|
||||
return view
|
||||
}()
|
||||
|
||||
private let shutterView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(0x17181F)
|
||||
view.isHidden = true
|
||||
return view
|
||||
}()
|
||||
|
||||
private let avatarImageView: UIImageView = {
|
||||
let imageView = UIImageView(frame: .zero)
|
||||
imageView.layer.masksToBounds = true
|
||||
imageView.isHidden = true
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private let userStatusView: RoomUserStatusView = {
|
||||
let view = RoomUserStatusView(frame: .zero)
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
viewModel = RoomVideoFloatViewModel()
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
NSObject.cancelPreviousPerformRequests(withTarget: self)
|
||||
debugPrint("deinit:\(self)")
|
||||
}
|
||||
|
||||
// MARK: - view layout
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
backgroundColor = .clear
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
super.draw(rect)
|
||||
roundedRect(rect: bounds,
|
||||
byRoundingCorners: .allCorners,
|
||||
cornerRadii: CGSize(width: 10, height: 10))
|
||||
avatarImageView.roundedCircle(rect: avatarImageView.bounds)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
if !isDraging {
|
||||
self.center = adsorption(centerPoint: self.center)
|
||||
}
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(renderView)
|
||||
addSubview(shutterView)
|
||||
addSubview(avatarImageView)
|
||||
addSubview(userStatusView)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
renderView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
shutterView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
avatarImageView.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.height.width.equalTo(50)
|
||||
}
|
||||
userStatusView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(5)
|
||||
make.bottom.equalToSuperview().offset(-5)
|
||||
make.width.lessThanOrEqualTo(self).multipliedBy(0.9)
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didPan(panGesture:)))
|
||||
addGestureRecognizer(panGesture)
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(didTap(sender:)))
|
||||
addGestureRecognizer(tap)
|
||||
viewModel.viewResponder = self
|
||||
viewModel.showFloatWindowViewVideo(renderView: renderView)
|
||||
setupViewState()
|
||||
}
|
||||
|
||||
private func setupViewState() {
|
||||
guard let userModel = viewModel.engineManager.store.attendeeList.first(where: { $0.userId == viewModel.userId }) else { return }
|
||||
let placeholder = UIImage(named: "room_default_user", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
avatarImageView.sd_setImage(with: URL(string: userModel.avatarUrl), placeholderImage: placeholder)
|
||||
userStatusView.updateUserVolume(hasAudio: userModel.hasAudioStream, volume: userModel.userVoiceVolume)
|
||||
}
|
||||
|
||||
@objc func didTap(sender: UIView) {
|
||||
viewModel.showRoomMainView()
|
||||
}
|
||||
|
||||
@objc func didPan(panGesture: UIPanGestureRecognizer) {
|
||||
guard let viewSuperview = superview else { return }
|
||||
let moveState = panGesture.state
|
||||
let viewCenter = center
|
||||
switch moveState {
|
||||
case .changed:
|
||||
isDraging = true
|
||||
let point = panGesture.translation(in: viewSuperview)
|
||||
center = CGPoint(x: viewCenter.x + point.x, y: viewCenter.y + point.y)
|
||||
break
|
||||
case .ended:
|
||||
let point = panGesture.translation(in: viewSuperview)
|
||||
let newPoint = CGPoint(x: viewCenter.x + point.x, y: viewCenter.y + point.y)
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
self.center = self.adsorption(centerPoint: newPoint)
|
||||
}
|
||||
isDraging = false
|
||||
break
|
||||
default: break
|
||||
}
|
||||
panGesture.setTranslation(.zero, in: viewSuperview)
|
||||
}
|
||||
|
||||
class func show(width: CGFloat = 100, height: CGFloat = 180) {
|
||||
DispatchQueue.main.async {
|
||||
guard let currentWindow = RoomRouter.getCurrentWindow() else { return }
|
||||
let roomFloatView = RoomVideoFloatView()
|
||||
currentWindow.addSubview(roomFloatView)
|
||||
roomFloatView.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-5)
|
||||
make.bottom.equalToSuperview().offset(-100)
|
||||
make.width.equalTo(width)
|
||||
make.height.equalTo(height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class func dismiss() {
|
||||
DispatchQueue.main.async {
|
||||
guard let currentWindow = RoomRouter.getCurrentWindow() else { return }
|
||||
for view in currentWindow.subviews where view is RoomVideoFloatView {
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func adsorption(centerPoint: CGPoint) -> CGPoint {
|
||||
guard let viewSuperview = superview else { return centerPoint }
|
||||
let limitMargin = 5.0
|
||||
let frame = self.frame
|
||||
let point = CGPoint(x: centerPoint.x - frame.width / 2, y: centerPoint.y - frame.height / 2)
|
||||
var newPoint = point
|
||||
if centerPoint.x < (viewSuperview.frame.width / 2) {
|
||||
newPoint.x = limitMargin
|
||||
} else {
|
||||
newPoint.x = viewSuperview.frame.width - frame.width - limitMargin
|
||||
}
|
||||
if point.y <= limitMargin {
|
||||
newPoint.y = limitMargin
|
||||
} else if (point.y + frame.height) > (viewSuperview.frame.height - limitMargin) {
|
||||
newPoint.y = viewSuperview.frame.height - frame.height - limitMargin
|
||||
}
|
||||
return CGPoint(x: newPoint.x + frame.width / 2, y: newPoint.y + frame.height / 2)
|
||||
}
|
||||
|
||||
private func resetVolume() {
|
||||
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(resetVolumeView), object: nil)
|
||||
perform(#selector(resetVolumeView), with: nil, afterDelay: 1)
|
||||
}
|
||||
|
||||
@objc func resetVolumeView() {
|
||||
guard let userItem = viewModel.getUserEntity(userId: viewModel.userId) else { return }
|
||||
userStatusView.updateUserVolume(hasAudio: userItem.hasAudioStream, volume: 0)
|
||||
}
|
||||
}
|
||||
|
||||
extension RoomVideoFloatView: RoomVideoFloatViewResponder {
|
||||
func makeToast(text: String) {
|
||||
RoomRouter.makeToastInCenter(toast: text, duration: 0.5)
|
||||
}
|
||||
|
||||
func updateUserStatus(user: UserEntity) {
|
||||
let placeholder = UIImage(named: "room_default_user", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
avatarImageView.sd_setImage(with: URL(string: user.avatarUrl), placeholderImage: placeholder)
|
||||
userStatusView.updateUserStatus(userModel: user)
|
||||
}
|
||||
|
||||
func updateUserAudioVolume(hasAudio: Bool, volume: Int) {
|
||||
userStatusView.updateUserVolume(hasAudio: hasAudio, volume: volume)
|
||||
resetVolume()
|
||||
}
|
||||
|
||||
func showAvatarImageView(isShow: Bool) {
|
||||
shutterView.isHidden = !isShow
|
||||
avatarImageView.isHidden = !isShow
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
//
|
||||
// LocalAudioView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/1/5.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class LocalAudioView: UIView {
|
||||
let viewModel: LocalAudioViewModel
|
||||
lazy var muteAudioButton : UIButton = {
|
||||
let button = UIButton()
|
||||
button.setImage(UIImage(named: "room_mic_on", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
|
||||
button.setImage(UIImage(named: "room_mic_off", in: tuiRoomKitBundle(), compatibleWith: nil), for: .selected)
|
||||
button.isSelected = viewModel.checkMuteAudioSelectedState()
|
||||
button.backgroundColor = UIColor(0x2A2D38)
|
||||
button.layer.cornerRadius = 12
|
||||
return button
|
||||
}()
|
||||
|
||||
init(viewModel: LocalAudioViewModel) {
|
||||
self.viewModel = viewModel
|
||||
super.init(frame: .zero)
|
||||
self.transform = CGAffineTransform(translationX: 0, y: kScreenHeight)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit:\(self)")
|
||||
}
|
||||
|
||||
// MARK: - view layout
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
addSubview(muteAudioButton)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
muteAudioButton.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
private func bindInteraction() {
|
||||
viewModel.viewResponder = self
|
||||
muteAudioButton.addTarget(self, action: #selector(muteAudioAction(sender:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc func muteAudioAction(sender: UIButton) {
|
||||
viewModel.muteAudioAction()
|
||||
}
|
||||
|
||||
func show() {
|
||||
UIView.animate(withDuration: 0.3) { [weak self] () in
|
||||
guard let self = self else { return }
|
||||
self.transform = .identity
|
||||
} completion: { _ in
|
||||
}
|
||||
}
|
||||
|
||||
func hide() {
|
||||
self.transform = CGAffineTransform(translationX: 0, y: kScreenHeight)
|
||||
}
|
||||
}
|
||||
|
||||
extension LocalAudioView: LocalAudioViewModelResponder {
|
||||
func updateMuteAudioButton(isSelected: Bool) {
|
||||
muteAudioButton.isSelected = isSelected
|
||||
}
|
||||
func makeToast(text: String) {
|
||||
RoomRouter.makeToastInCenter(toast: text, duration: 1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
//
|
||||
// MediaSettingView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2023/1/16.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class MediaSettingView: UIView {
|
||||
let viewModel: MediaSettingViewModel
|
||||
lazy var setUpTableView: UITableView = {
|
||||
let tableView = UITableView(frame: .zero, style: .grouped)
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
tableView.backgroundColor = UIColor(0x1B1E26)
|
||||
tableView.register(UserListCell.self, forCellReuseIdentifier: "MediaSettingViewCell")
|
||||
tableView.sectionHeaderHeight = 48.scale375()
|
||||
return tableView
|
||||
}()
|
||||
|
||||
init(viewModel: MediaSettingViewModel) {
|
||||
self.viewModel = viewModel
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
isViewReady = true
|
||||
backgroundColor = UIColor(0x1B1E26)
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
addSubview(setUpTableView)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
setUpTableView.snp.makeConstraints { make in
|
||||
make.top.bottom.equalToSuperview()
|
||||
make.leading.equalToSuperview().offset(13.scale375())
|
||||
make.trailing.equalToSuperview().offset(-13.scale375())
|
||||
}
|
||||
}
|
||||
|
||||
private func bindInteraction() {
|
||||
viewModel.viewResponder = self
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
|
||||
extension MediaSettingView: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
if section == 0 {
|
||||
return viewModel.videoItems.count
|
||||
} else if section == 1 {
|
||||
return viewModel.audioItems.count
|
||||
} else if section == 2 {
|
||||
return viewModel.otherItems.count
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return viewModel.topItems.count
|
||||
}
|
||||
}
|
||||
|
||||
extension MediaSettingView: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
var itemData = ListCellItemData()
|
||||
if indexPath.section == 0, indexPath.row < viewModel.videoItems.count {
|
||||
itemData = viewModel.videoItems[indexPath.row]
|
||||
} else if indexPath.section == 1 {
|
||||
itemData = viewModel.audioItems[indexPath.row]
|
||||
} else if indexPath.section == 2 {
|
||||
itemData = viewModel.otherItems[indexPath.row]
|
||||
}
|
||||
let cell = MediaSettingViewCell(itemData: itemData)
|
||||
cell.selectionStyle = .none
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return 55.scale375()
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
let headerLabel = UILabel()
|
||||
headerLabel.font = UIFont.systemFont(ofSize: 14, weight: .regular)
|
||||
headerLabel.textColor = UIColor(0xD8D8D8)
|
||||
headerLabel.textAlignment = isRTL ? .right : .left
|
||||
headerLabel.text = viewModel.topItems[safe: section]
|
||||
return headerLabel
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
let rows = tableView.numberOfRows(inSection: indexPath.section)
|
||||
if indexPath.row == 0 || indexPath.row == rows - 1 {
|
||||
var corner = UIRectCorner()
|
||||
if rows == 1 {
|
||||
corner = .allCorners
|
||||
} else if indexPath.row == 0 {
|
||||
corner = [.topLeft, .topRight]
|
||||
} else if indexPath.row == rows - 1 {
|
||||
corner = [.bottomLeft, .bottomRight]
|
||||
}
|
||||
cell.roundedRect(rect: cell.bounds,
|
||||
byRoundingCorners: corner,
|
||||
cornerRadii: CGSize(width: 12, height: 12))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MediaSettingView: MediaSettingViewEventResponder {
|
||||
func showFrameRateAlert() {
|
||||
let frameRateAlert = VideoChoicePanel()
|
||||
frameRateAlert.titleText = .frameRateText
|
||||
frameRateAlert.dataSource = viewModel.frameRateArray
|
||||
frameRateAlert.selectIndex = viewModel.getCurrentFrameRateIndex()
|
||||
frameRateAlert.didSelectItem = { [weak self] index in
|
||||
guard let `self` = self else { return }
|
||||
self.viewModel.changeFrameRateAction(index: index)
|
||||
}
|
||||
frameRateAlert.show(rootView: self)
|
||||
}
|
||||
|
||||
func showResolutionAlert() {
|
||||
let resolutionAlert = VideoChoicePanel()
|
||||
resolutionAlert.titleText = .resolutionText
|
||||
resolutionAlert.dataSource = viewModel.resolutionNameItems
|
||||
resolutionAlert.selectIndex = viewModel.getCurrentResolutionIndex()
|
||||
resolutionAlert.didSelectItem = { [weak self] index in
|
||||
guard let `self` = self else { return }
|
||||
self.viewModel.changeResolutionAction(index: index)
|
||||
}
|
||||
resolutionAlert.show(rootView: self)
|
||||
}
|
||||
|
||||
func showQualityView() {
|
||||
let qualityInfoPanel = QualityInfoPanel()
|
||||
qualityInfoPanel.show(rootView: self)
|
||||
}
|
||||
|
||||
func updateStackView(item: ListCellItemData) {
|
||||
for view in setUpTableView.visibleCells where view is MediaSettingViewCell {
|
||||
guard let cell = view as? MediaSettingViewCell else { continue }
|
||||
guard cell.itemData.type == item.type else { continue }
|
||||
cell.updateStackView(item: item)
|
||||
}
|
||||
}
|
||||
func makeToast(text: String) {
|
||||
RoomRouter.makeToastInCenter(toast: text, duration: 1)
|
||||
}
|
||||
}
|
||||
|
||||
class MediaSettingViewCell: UITableViewCell {
|
||||
var itemData: ListCellItemData
|
||||
lazy var listCell: ListCellItemView = {
|
||||
let view = ListCellItemView(itemData: itemData)
|
||||
return view
|
||||
}()
|
||||
init(itemData: ListCellItemData) {
|
||||
self.itemData = itemData
|
||||
super.init(style: .default, reuseIdentifier: "UserListCell")
|
||||
}
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
isViewReady = true
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
contentView.addSubview(listCell)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
listCell.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(13.scale375())
|
||||
make.trailing.equalToSuperview().offset(-13.scale375())
|
||||
make.top.bottom.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
private func bindInteraction() {
|
||||
backgroundColor = UIColor(0x242934)
|
||||
}
|
||||
|
||||
func updateStackView(item: ListCellItemData) {
|
||||
listCell.setupViewState(item: item)
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var resolutionText: String {
|
||||
localized("Resolution")
|
||||
}
|
||||
static var frameRateText: String {
|
||||
localized("Frame Rate")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
//
|
||||
// QualityInfoPanel.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by CY zhao on 2024/4/19.
|
||||
// Copyright © 2024 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class QualityInfoPanel: UIView {
|
||||
var viewModel: QualityInfoViewModel = QualityInfoViewModel()
|
||||
let landscapeHight: CGFloat = min(kScreenWidth, kScreenHeight)
|
||||
let portraitHight: CGFloat = 411.scale375Height()
|
||||
private let arrowViewHeight: CGFloat = 35.0
|
||||
private let cellHeight: CGFloat = 33.0
|
||||
private let lineViewHorizontalMargin = 16.0
|
||||
private let headerHeight = 48
|
||||
private var currentLandscape: Bool = isLandscape
|
||||
|
||||
let bgView: UIView = {
|
||||
let view = UIView(frame: .zero)
|
||||
view.backgroundColor = .black
|
||||
view.alpha = 0.6
|
||||
return view
|
||||
}()
|
||||
|
||||
let contentView: UIView = {
|
||||
let view = UIView(frame: .zero)
|
||||
view.backgroundColor = UIColor(0x22262E)
|
||||
view.layer.cornerRadius = 12
|
||||
return view
|
||||
}()
|
||||
|
||||
private let dropArrowView : UIView = {
|
||||
let view = UIView()
|
||||
return view
|
||||
}()
|
||||
|
||||
private let dropArrowImageView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.image = UIImage(named: "room_drop_arrow", in:tuiRoomKitBundle(), compatibleWith: nil)
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var tableView: UITableView = {
|
||||
let tableView = UITableView(frame: .zero, style: .grouped)
|
||||
tableView.separatorStyle = .none
|
||||
tableView.backgroundColor = UIColor(0x22262E)
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
tableView.register(QualityTableViewCell.self,
|
||||
forCellReuseIdentifier: "QualityTableViewCell")
|
||||
tableView.sectionHeaderHeight = headerHeight.scale375()
|
||||
if #available(iOS 15.0, *) {
|
||||
tableView.sectionHeaderTopPadding = 0
|
||||
}
|
||||
return tableView
|
||||
}()
|
||||
|
||||
var willDismiss: (() -> Void)?
|
||||
var didDismiss: (() -> Void)?
|
||||
|
||||
override init(frame: CGRect = .zero) {
|
||||
super.init(frame: frame)
|
||||
contentView.transform = CGAffineTransform(translationX: 0, y: kScreenHeight)
|
||||
alpha = 0
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private var isViewReady = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else {
|
||||
return
|
||||
}
|
||||
isViewReady = true
|
||||
self.viewModel.viewResponder = self
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
}
|
||||
|
||||
func show(rootView: UIView) {
|
||||
rootView.addSubview(self)
|
||||
self.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
UIView.animate(withDuration: 0.3) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.alpha = 1
|
||||
self.contentView.transform = .identity
|
||||
}
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
if let action = willDismiss {
|
||||
action()
|
||||
}
|
||||
UIView.animate(withDuration: 0.3) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.alpha = 0
|
||||
self.contentView.transform = CGAffineTransform(translationX: 0, y: kScreenHeight)
|
||||
} completion: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
if let action = self.didDismiss {
|
||||
action()
|
||||
}
|
||||
self.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
guard currentLandscape != isLandscape else { return }
|
||||
setupViewOrientation(isLandscape: isLandscape)
|
||||
currentLandscape = isLandscape
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(bgView)
|
||||
addSubview(contentView)
|
||||
dropArrowView.addSubview(dropArrowImageView)
|
||||
contentView.addSubview(dropArrowView)
|
||||
contentView.addSubview(tableView)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
bgView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
setupViewOrientation(isLandscape: isLandscape)
|
||||
dropArrowView.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(arrowViewHeight)
|
||||
}
|
||||
dropArrowImageView.snp.makeConstraints { make in
|
||||
make.centerX.centerY.equalToSuperview()
|
||||
make.width.equalTo(24.scale375())
|
||||
make.height.equalTo(3.scale375())
|
||||
}
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.top.equalTo(dropArrowView.snp.bottom)
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
let dropArrowTap = UITapGestureRecognizer(target: self, action: #selector(dropDownPopUpViewAction(sender:)))
|
||||
dropArrowView.addGestureRecognizer(dropArrowTap)
|
||||
dropArrowView.isUserInteractionEnabled = true
|
||||
}
|
||||
|
||||
private func setupViewOrientation(isLandscape: Bool) {
|
||||
contentView.snp.remakeConstraints { make in
|
||||
if isLandscape {
|
||||
make.height.equalTo(landscapeHight)
|
||||
} else {
|
||||
make.height.equalTo(portraitHight)
|
||||
}
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func dropDownPopUpViewAction(sender: UIView) {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
|
||||
extension QualityInfoPanel: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return viewModel.sections[section].items.count
|
||||
}
|
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return viewModel.sections.count
|
||||
}
|
||||
}
|
||||
|
||||
extension QualityInfoPanel: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "QualityTableViewCell", for: indexPath)
|
||||
if let qualityCell = cell as? QualityTableViewCell {
|
||||
qualityCell.setCellModel(model: self.viewModel.sections[indexPath.section].items[indexPath.row])
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return cellHeight.scale375()
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
let headerView = UIView()
|
||||
let headerLabel = UILabel()
|
||||
headerLabel.font = UIFont.systemFont(ofSize: 14, weight: .regular)
|
||||
headerLabel.textColor = UIColor(0x99A2B2)
|
||||
headerLabel.textAlignment = isRTL ? .right : .left
|
||||
headerLabel.text = viewModel.sections[safe: section]?.titleText
|
||||
headerView.addSubview(headerLabel)
|
||||
headerLabel.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(16)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
return headerView
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
|
||||
if section == self.viewModel.sections.count - 1 {
|
||||
return 0
|
||||
}
|
||||
return 0.5
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
|
||||
if section == self.viewModel.sections.count - 1 {
|
||||
return nil
|
||||
}
|
||||
let footerView = UIView()
|
||||
let lineView = UIView(frame: CGRect(x: lineViewHorizontalMargin,
|
||||
y: 0,
|
||||
width: tableView.frame.size.width - 2 * lineViewHorizontalMargin,
|
||||
height: 0.5))
|
||||
lineView.backgroundColor = UIColor(0xB2BBD1)
|
||||
footerView.addSubview(lineView)
|
||||
return footerView
|
||||
}
|
||||
}
|
||||
|
||||
extension QualityInfoPanel: QualityViewResponder {
|
||||
func reloadData() {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
class QualityTableViewCell: UITableViewCell {
|
||||
let titleLabel: UILabel = {
|
||||
let label = UILabel(frame: .zero)
|
||||
label.font = UIFont(name: "PingFangSC-Regular", size: 16)
|
||||
label.textColor = UIColor(0xE7ECF6)
|
||||
label.sizeToFit()
|
||||
return label
|
||||
}()
|
||||
|
||||
let normalInfoLabel: UILabel = {
|
||||
let label = UILabel(frame: .zero)
|
||||
label.font = UIFont(name: "PingFangSC-Regular", size: 16)
|
||||
label.textColor = UIColor(0xE7ECF6)
|
||||
label.isHidden = true
|
||||
label.sizeToFit()
|
||||
return label
|
||||
}()
|
||||
|
||||
let upInfoLabel: UILabel = {
|
||||
let label = UILabel(frame: .zero)
|
||||
label.font = UIFont(name: "PingFangSC-Regular", size: 16)
|
||||
label.textColor = UIColor(0xE7ECF6)
|
||||
label.isHidden = true
|
||||
label.sizeToFit()
|
||||
return label
|
||||
}()
|
||||
|
||||
let uplinkImageView: UIImageView = {
|
||||
let norImage = UIImage(named: "room_uplink_arrow", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
let imageView = UIImageView(image: norImage)
|
||||
imageView.isHidden = true
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let downInfoLabel: UILabel = {
|
||||
let label = UILabel(frame: .zero)
|
||||
label.font = UIFont(name: "PingFangSC-Regular", size: 16)
|
||||
label.textColor = UIColor(0xE7ECF6)
|
||||
label.isHidden = true
|
||||
label.sizeToFit()
|
||||
return label
|
||||
}()
|
||||
|
||||
let downlinkImageView: UIImageView = {
|
||||
let norImage = UIImage(named: "room_downlink_arrow", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
let imageView = UIImageView(image: norImage)
|
||||
imageView.isHidden = true
|
||||
return imageView
|
||||
}()
|
||||
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
}
|
||||
|
||||
private var isViewReady = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
backgroundColor = .clear
|
||||
selectionStyle = .none
|
||||
guard !isViewReady else {
|
||||
return
|
||||
}
|
||||
isViewReady = true
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(normalInfoLabel)
|
||||
contentView.addSubview(downInfoLabel)
|
||||
contentView.addSubview(downlinkImageView)
|
||||
contentView.addSubview(upInfoLabel)
|
||||
contentView.addSubview(uplinkImageView)
|
||||
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.leading.equalToSuperview().offset(16)
|
||||
}
|
||||
normalInfoLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.trailing.equalToSuperview().offset(-16)
|
||||
}
|
||||
downlinkImageView.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(16)
|
||||
make.centerY.equalToSuperview()
|
||||
make.trailing.equalToSuperview().offset(-16)
|
||||
}
|
||||
downInfoLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.trailing.equalTo(downlinkImageView.snp.leading).offset(-4)
|
||||
}
|
||||
uplinkImageView.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(16)
|
||||
make.centerY.equalToSuperview()
|
||||
make.trailing.equalTo(downInfoLabel.snp.leading).offset(-16)
|
||||
}
|
||||
upInfoLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.trailing.equalTo(uplinkImageView.snp.leading).offset(-4)
|
||||
}
|
||||
}
|
||||
|
||||
func setCellModel(model: QualityCellModel) {
|
||||
self.titleLabel.text = model.titleText
|
||||
if model.type == .upDown {
|
||||
showUpDownInfo(isShow: true)
|
||||
showNormalInfo(isShow: false)
|
||||
upInfoLabel.text = model.uplinkString
|
||||
downInfoLabel.text = model.downlinkString
|
||||
} else {
|
||||
showUpDownInfo(isShow: false)
|
||||
showNormalInfo(isShow: true)
|
||||
normalInfoLabel.text = model.normalString
|
||||
}
|
||||
}
|
||||
|
||||
private func showUpDownInfo(isShow: Bool) {
|
||||
upInfoLabel.isHidden = !isShow
|
||||
uplinkImageView.isHidden = !isShow
|
||||
downInfoLabel.isHidden = !isShow
|
||||
downlinkImageView.isHidden = !isShow
|
||||
}
|
||||
|
||||
private func showNormalInfo(isShow: Bool) {
|
||||
normalInfoLabel.isHidden = !isShow
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
//
|
||||
// VideoChoicePanel.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2023/1/17.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
// Video resolution or frame rate selection panel
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: Resolution
|
||||
class VideoChoicePanel: AlertContentView {
|
||||
var dataSource: [String] = []
|
||||
var selectIndex = 3
|
||||
var titleText: String = ""
|
||||
var didSelectItem: ((_ index: Int) -> Void)?
|
||||
|
||||
let tableView: UITableView = {
|
||||
let tableView = UITableView(frame: .zero, style: .plain)
|
||||
tableView.separatorStyle = .none
|
||||
tableView.backgroundColor = UIColor(0x22262E)
|
||||
return tableView
|
||||
}()
|
||||
|
||||
override init(frame: CGRect = .zero) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func constructViewHierarchy() {
|
||||
super.constructViewHierarchy()
|
||||
contentView.addSubview(tableView)
|
||||
}
|
||||
|
||||
override func activateConstraints() {
|
||||
super.activateConstraints()
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(space.scale375Height())
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
override func bindInteraction() {
|
||||
super.bindInteraction()
|
||||
titleLabel.text = titleText
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
tableView.register(ResolutionTableViewCell.self,
|
||||
forCellReuseIdentifier: "ResolutionTableViewCell")
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
|
||||
extension VideoChoicePanel: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return dataSource.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "ResolutionTableViewCell", for: indexPath)
|
||||
if let scell = cell as? ResolutionTableViewCell {
|
||||
scell.titleLabel.text = dataSource[indexPath.row]
|
||||
scell.isSelected = indexPath.row == selectIndex
|
||||
}
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
extension VideoChoicePanel: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
selectIndex = indexPath.row
|
||||
tableView.reloadSections(IndexSet(integer: 0), with: .none)
|
||||
if let action = didSelectItem {
|
||||
action(selectIndex)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
class ResolutionTableViewCell: UITableViewCell {
|
||||
let titleLabel: UILabel = {
|
||||
let label = UILabel(frame: .zero)
|
||||
label.font = UIFont(name: "PingFangSC-Regular", size: 16)
|
||||
label.textColor = UIColor(0xD1D9EC)
|
||||
return label
|
||||
}()
|
||||
|
||||
let checkboxImageView: UIImageView = {
|
||||
let norImage = UIImage(named: "room_checkbox_sel", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
let imageView = UIImageView(image: norImage)
|
||||
imageView.isHidden = true
|
||||
return imageView
|
||||
}()
|
||||
|
||||
override var isSelected: Bool {
|
||||
didSet {
|
||||
checkboxImageView.isHidden = !isSelected
|
||||
}
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
}
|
||||
|
||||
private var isViewReady = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
backgroundColor = .clear
|
||||
selectionStyle = .none
|
||||
guard !isViewReady else {
|
||||
return
|
||||
}
|
||||
isViewReady = true
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(checkboxImageView)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.leading.equalToSuperview().offset(20)
|
||||
}
|
||||
checkboxImageView.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.trailing.equalToSuperview().offset(-20)
|
||||
make.size.equalTo(CGSize(width: 16, height: 16))
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Base
|
||||
class AlertContentView: UIView {
|
||||
let space: Int = 16
|
||||
let landscapeHight: CGFloat = min(kScreenWidth, kScreenHeight)
|
||||
let portraitHight: CGFloat = 718.scale375Height()
|
||||
private var currentLandscape: Bool = isLandscape
|
||||
let bgView: UIView = {
|
||||
let view = UIView(frame: .zero)
|
||||
view.backgroundColor = .black
|
||||
view.alpha = 0.6
|
||||
return view
|
||||
}()
|
||||
|
||||
let contentView: UIView = {
|
||||
let view = UIView(frame: .zero)
|
||||
view.backgroundColor = UIColor(0x22262E)
|
||||
view.layer.cornerRadius = 12
|
||||
return view
|
||||
}()
|
||||
|
||||
let backButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setImage(UIImage(named: "room_back_white", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
|
||||
button.backgroundColor = .clear
|
||||
return button
|
||||
}()
|
||||
|
||||
let titleLabel: UILabel = {
|
||||
let label = UILabel(frame: .zero)
|
||||
label.textColor = UIColor(0xD1D9EC)
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
label.font = UIFont(name: "PingFangSC-Medium", size: 16)
|
||||
return label
|
||||
}()
|
||||
|
||||
var willDismiss: (() -> Void)?
|
||||
var didDismiss: (() -> Void)?
|
||||
|
||||
override init(frame: CGRect = .zero) {
|
||||
super.init(frame: frame)
|
||||
contentView.transform = CGAffineTransform(translationX: 0, y: kScreenHeight)
|
||||
alpha = 0
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private var isViewReady = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else {
|
||||
return
|
||||
}
|
||||
isViewReady = true
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
}
|
||||
|
||||
func show(rootView: UIView) {
|
||||
rootView.addSubview(self)
|
||||
self.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
UIView.animate(withDuration: 0.3) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.alpha = 1
|
||||
self.contentView.transform = .identity
|
||||
}
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
if let action = willDismiss {
|
||||
action()
|
||||
}
|
||||
UIView.animate(withDuration: 0.3) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.alpha = 0
|
||||
self.contentView.transform = CGAffineTransform(translationX: 0, y: kScreenHeight)
|
||||
} completion: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
if let action = self.didDismiss {
|
||||
action()
|
||||
}
|
||||
self.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
guard let point = touches.first?.location(in: self) else {
|
||||
return
|
||||
}
|
||||
let backButtonFrame = backButton.frame.inset(by: UIEdgeInsets.init(top: -space.scale375Height(), left: -space.scale375(), bottom: -space.scale375Height(), right: -space.scale375()))
|
||||
if !contentView.frame.contains(point) || backButtonFrame.contains(point) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
guard currentLandscape != isLandscape else { return }
|
||||
setupViewOrientation(isLandscape: isLandscape)
|
||||
currentLandscape = isLandscape
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(bgView)
|
||||
addSubview(contentView)
|
||||
contentView.addSubview(backButton)
|
||||
contentView.addSubview(titleLabel)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
bgView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
setupViewOrientation(isLandscape: isLandscape)
|
||||
backButton.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(16)
|
||||
make.leading.equalToSuperview().offset(space.scale375())
|
||||
make.centerY.equalTo(titleLabel)
|
||||
}
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(space.scale375Height())
|
||||
make.centerX.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
backButton.addTarget(self, action: #selector(backAction(sender:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
private func setupViewOrientation(isLandscape: Bool) {
|
||||
contentView.snp.remakeConstraints { make in
|
||||
if isLandscape {
|
||||
make.height.equalTo(landscapeHight)
|
||||
} else {
|
||||
make.height.equalTo(portraitHight)
|
||||
}
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func backAction(sender: UIButton) {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
//
|
||||
// PopUpViewController.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2023/1/12.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import TUICore
|
||||
|
||||
protocol PopUpViewResponder: AnyObject {
|
||||
func updateAlertTransitionPosition(position: AlertTransitionAnimator.AlertTransitionPosition)
|
||||
}
|
||||
|
||||
class PopUpView: UIView {
|
||||
let viewModel: PopUpViewModel
|
||||
var rootView: UIView?
|
||||
weak var responder: PopUpViewResponder?
|
||||
private let arrowViewHeight: CGFloat = 35.0
|
||||
private var currentLandscape: Bool = isLandscape
|
||||
|
||||
private let panelControl : UIControl = {
|
||||
let control = UIControl()
|
||||
control.backgroundColor = .clear
|
||||
return control
|
||||
}()
|
||||
|
||||
private let dropArrowView : UIView = {
|
||||
let view = UIView()
|
||||
return view
|
||||
}()
|
||||
|
||||
private let dropArrowImageView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.image = UIImage(named: "room_drop_arrow",in:tuiRoomKitBundle(),compatibleWith: nil)
|
||||
return view
|
||||
}()
|
||||
|
||||
private let rightArrowView : UIView = {
|
||||
let view = UIView()
|
||||
return view
|
||||
}()
|
||||
|
||||
private let rightArrowImageView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.image = UIImage(named: "room_right_arrow",in:tuiRoomKitBundle(),compatibleWith: nil)
|
||||
return view
|
||||
}()
|
||||
|
||||
private let backgroundView: UIView = {
|
||||
let view = UIView()
|
||||
return view
|
||||
}()
|
||||
|
||||
init(viewModel: PopUpViewModel) {
|
||||
self.viewModel = viewModel
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
isViewReady = true
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(panelControl)
|
||||
addSubview(backgroundView)
|
||||
backgroundView.addSubview(dropArrowView)
|
||||
backgroundView.addSubview(rightArrowView)
|
||||
dropArrowView.addSubview(dropArrowImageView)
|
||||
rightArrowView.addSubview(rightArrowImageView)
|
||||
setupViewState()
|
||||
guard let rootView = rootView else { return }
|
||||
backgroundView.addSubview(rootView)
|
||||
backgroundView.layer.cornerRadius = 15
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
panelControl.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
setupViewOrientation(isLandscape: isLandscape)
|
||||
dropArrowView.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(arrowViewHeight)
|
||||
}
|
||||
dropArrowImageView.snp.makeConstraints { make in
|
||||
make.centerX.centerY.equalToSuperview()
|
||||
make.width.equalTo(24.scale375())
|
||||
make.height.equalTo(3.scale375())
|
||||
}
|
||||
rightArrowView.snp.makeConstraints { make in
|
||||
make.top.leading.bottom.equalToSuperview()
|
||||
make.width.equalTo(arrowViewHeight)
|
||||
}
|
||||
rightArrowImageView.snp.makeConstraints { make in
|
||||
make.centerY.centerX.equalToSuperview()
|
||||
make.width.equalTo(3.scale375())
|
||||
make.height.equalTo(24.scale375())
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
backgroundView.backgroundColor = viewModel.backgroundColor
|
||||
let dropArrowTap = UITapGestureRecognizer(target: self, action: #selector(dropDownPopUpViewAction(sender:)))
|
||||
dropArrowView.addGestureRecognizer(dropArrowTap)
|
||||
dropArrowView.isUserInteractionEnabled = true
|
||||
let rightArrowTap = UITapGestureRecognizer(target: self, action: #selector(dropDownPopUpViewAction(sender:)))
|
||||
rightArrowView.addGestureRecognizer(rightArrowTap)
|
||||
rightArrowView.isUserInteractionEnabled = true
|
||||
panelControl.addTarget(self, action: #selector(panelControlAction), for: .touchUpInside)
|
||||
}
|
||||
|
||||
func setupViewState() {
|
||||
switch viewModel.viewType {
|
||||
case .roomInfoViewType:
|
||||
let model = RoomInfoViewModel()
|
||||
rootView = RoomInfoView(viewModel: model)
|
||||
case .mediaSettingViewType:
|
||||
let model = MediaSettingViewModel()
|
||||
let view = MediaSettingView(viewModel: model)
|
||||
rootView = view
|
||||
case .userListViewType:
|
||||
let model = UserListViewModel()
|
||||
rootView = UserListView(viewModel: model)
|
||||
case .raiseHandApplicationListViewType:
|
||||
let model = RaiseHandApplicationListViewModel()
|
||||
rootView = RaiseHandApplicationListView(viewModel: model)
|
||||
case .transferMasterViewType:
|
||||
let model = TransferMasterViewModel()
|
||||
viewModel.viewResponder = model
|
||||
rootView = TransferMasterView(viewModel: model)
|
||||
case .QRCodeViewType:
|
||||
let model = QRCodeViewModel(urlString: "https://web.sdk.qcloud.com/component/tuiroom/index.html#/" + "#/room?roomId=" +
|
||||
EngineManager.shared.store.roomInfo.roomId)
|
||||
rootView = QRCodeView(viewModel: model)
|
||||
case .inviteViewType:
|
||||
rootView = InviteView()
|
||||
case .inviteMemberViewType:
|
||||
let model = MemberInviteViewModel()
|
||||
rootView = MemberInviteView(viewModel: model)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
@objc func dropDownPopUpViewAction(sender: UIView) {
|
||||
RoomRouter.shared.dismissPopupViewController()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
guard currentLandscape != isLandscape else { return }
|
||||
setupViewOrientation(isLandscape: isLandscape)
|
||||
responder?.updateAlertTransitionPosition(position: isLandscape ? .right : .bottom)
|
||||
currentLandscape = isLandscape
|
||||
}
|
||||
|
||||
func setupViewOrientation(isLandscape: Bool) {
|
||||
let width = min(kScreenHeight, kScreenWidth)
|
||||
let height = max(kScreenHeight, kScreenWidth)
|
||||
if isLandscape {
|
||||
backgroundView.snp.remakeConstraints { make in
|
||||
make.width.equalTo(width + arrowViewHeight)
|
||||
make.top.equalToSuperview()
|
||||
make.height.equalToSuperview()
|
||||
make.trailing.equalTo(safeAreaLayoutGuide.snp.trailing)
|
||||
}
|
||||
} else {
|
||||
let currentHeight = min(viewModel.height + arrowViewHeight, height - arrowViewHeight)
|
||||
backgroundView.snp.remakeConstraints { make in
|
||||
make.width.bottom.equalToSuperview()
|
||||
make.height.equalTo(currentHeight)
|
||||
}
|
||||
}
|
||||
rootView?.snp.remakeConstraints { make in
|
||||
if isLandscape {
|
||||
make.leading.equalToSuperview().offset(arrowViewHeight)
|
||||
make.trailing.top.bottom.equalToSuperview()
|
||||
} else {
|
||||
let currentHeight = min(viewModel.height, height - 2*arrowViewHeight)
|
||||
make.height.equalTo(currentHeight)
|
||||
make.trailing.leading.bottom.equalToSuperview()
|
||||
}
|
||||
}
|
||||
rightArrowView.isHidden = !isLandscape
|
||||
dropArrowView.isHidden = isLandscape
|
||||
}
|
||||
|
||||
|
||||
@objc func panelControlAction() {
|
||||
viewModel.panelControlAction()
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// PopUpViewController.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2023/1/12.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
protocol PopUpViewModelFactory {
|
||||
func makeRootViewModel(viewType: PopUpViewType, height:CGFloat, backgroundColor: UIColor) -> PopUpViewModel
|
||||
}
|
||||
|
||||
class PopUpViewController: UIViewController {
|
||||
let viewModel: PopUpViewModel
|
||||
var duration = 0.5
|
||||
var alertTransitionStyle: AlertTransitionAnimator.AlertTransitionStyle = .present
|
||||
var alertTransitionPosition: AlertTransitionAnimator.AlertTransitionPosition = .bottom
|
||||
var transitionAnimator: AlertTransitionAnimator?
|
||||
override var shouldAutorotate: Bool {
|
||||
return true
|
||||
}
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return .allButUpsideDown
|
||||
}
|
||||
init(popUpViewModelFactory: PopUpViewModelFactory, viewType: PopUpViewType, height: CGFloat, backgroundColor: UIColor) {
|
||||
viewModel = popUpViewModelFactory.makeRootViewModel(viewType: viewType, height: height, backgroundColor: backgroundColor)
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
modalPresentationStyle = .custom
|
||||
transitioningDelegate = self
|
||||
if isLandscape {
|
||||
self.alertTransitionPosition = .right
|
||||
} else {
|
||||
self.alertTransitionPosition = .bottom
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
let rootView = PopUpView(viewModel: viewModel)
|
||||
rootView.responder = self
|
||||
view = rootView
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
navigationController?.setNavigationBarHidden(true, animated: false)
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
|
||||
extension PopUpViewController: UIViewControllerTransitioningDelegate {
|
||||
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) ->
|
||||
UIViewControllerAnimatedTransitioning? {
|
||||
transitionAnimator = AlertTransitionAnimator()
|
||||
transitionAnimator?.alertTransitionStyle = .present
|
||||
transitionAnimator?.alertTransitionPosition = alertTransitionPosition
|
||||
transitionAnimator?.duration = duration
|
||||
return transitionAnimator
|
||||
}
|
||||
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
transitionAnimator?.alertTransitionStyle = .dismiss
|
||||
return transitionAnimator
|
||||
}
|
||||
}
|
||||
|
||||
extension PopUpViewController: PopUpViewResponder {
|
||||
func updateAlertTransitionPosition(position: AlertTransitionAnimator.AlertTransitionPosition) {
|
||||
transitionAnimator?.alertTransitionPosition = position
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
//
|
||||
// RaiseHandApplicationCell.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/5/7.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class RaiseHandApplicationCell: UITableViewCell {
|
||||
let attendeeModel: RequestEntity
|
||||
let viewModel: RaiseHandApplicationListViewModel
|
||||
|
||||
let avatarImageView: UIImageView = {
|
||||
let img = UIImageView()
|
||||
img.layer.cornerRadius = 20
|
||||
img.layer.masksToBounds = true
|
||||
return img
|
||||
}()
|
||||
|
||||
let userLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor(0xD5E0F2)
|
||||
label.backgroundColor = UIColor.clear
|
||||
label.textAlignment = isRTL ? .right : .left
|
||||
label.font = UIFont.systemFont(ofSize: 16, weight: .regular)
|
||||
label.numberOfLines = 1
|
||||
return label
|
||||
}()
|
||||
|
||||
let applyLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = .applyText
|
||||
label.textColor = UIColor(0x8F9AB2)
|
||||
label.backgroundColor = UIColor.clear
|
||||
label.textAlignment = isRTL ? .right : .left
|
||||
label.font = UIFont.systemFont(ofSize: 12)
|
||||
return label
|
||||
}()
|
||||
|
||||
let disagreeStageButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.backgroundColor = UIColor(0x6B758A).withAlphaComponent(0.3)
|
||||
button.setTitle(.disagreeSeatText, for: .normal)
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .regular)
|
||||
button.titleLabel?.adjustsFontSizeToFitWidth = true
|
||||
button.setTitleColor(UIColor(0xB2BBD1), for: .normal)
|
||||
button.layer.cornerRadius = 6
|
||||
button.clipsToBounds = true
|
||||
return button
|
||||
}()
|
||||
|
||||
let agreeStageButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.backgroundColor = UIColor(0x1C66E5)
|
||||
button.setTitle(.agreeSeatText, for: .normal)
|
||||
button.setTitleColor(.white, for: .normal)
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .regular)
|
||||
button.titleLabel?.adjustsFontSizeToFitWidth = true
|
||||
button.layer.cornerRadius = 6
|
||||
button.clipsToBounds = true
|
||||
return button
|
||||
}()
|
||||
|
||||
let downLineView : UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(0x4F586B).withAlphaComponent(0.3)
|
||||
return view
|
||||
}()
|
||||
|
||||
init(attendeeModel: RequestEntity ,viewModel: RaiseHandApplicationListViewModel) {
|
||||
self.attendeeModel = attendeeModel
|
||||
self.viewModel = viewModel
|
||||
super.init(style: .default, reuseIdentifier: "RaiseHandCell")
|
||||
}
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
contentView.addSubview(avatarImageView)
|
||||
contentView.addSubview(userLabel)
|
||||
contentView.addSubview(applyLabel)
|
||||
contentView.addSubview(agreeStageButton)
|
||||
contentView.addSubview(disagreeStageButton)
|
||||
contentView.addSubview(downLineView)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
avatarImageView.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(40.scale375())
|
||||
make.leading.equalToSuperview()
|
||||
make.top.equalToSuperview().offset(10.scale375Height())
|
||||
}
|
||||
agreeStageButton.snp.makeConstraints { make in
|
||||
make.width.equalTo(48.scale375())
|
||||
make.height.equalTo(28.scale375Height())
|
||||
make.trailing.equalToSuperview()
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
disagreeStageButton.snp.makeConstraints { make in
|
||||
make.trailing.equalTo(agreeStageButton.snp.leading).offset(-10)
|
||||
make.centerY.equalTo(agreeStageButton)
|
||||
make.width.height.equalTo(agreeStageButton)
|
||||
}
|
||||
userLabel.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(14.scale375Height())
|
||||
make.leading.equalTo(avatarImageView.snp.trailing).offset(12.scale375())
|
||||
make.width.equalTo(150.scale375())
|
||||
make.height.equalTo(22.scale375())
|
||||
}
|
||||
applyLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(userLabel.snp.bottom).offset(2.scale375Height())
|
||||
make.leading.equalTo(userLabel)
|
||||
}
|
||||
downLineView.snp.makeConstraints { make in
|
||||
make.leading.equalTo(userLabel)
|
||||
make.trailing.equalToSuperview()
|
||||
make.bottom.equalToSuperview()
|
||||
make.height.equalTo(1.scale375())
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
setupViewState(item: attendeeModel)
|
||||
agreeStageButton.addTarget(self, action: #selector(agreeStageAction(sender:)), for: .touchUpInside)
|
||||
disagreeStageButton.addTarget(self, action: #selector(disagreeStageAction(sender:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
func setupViewState(item: RequestEntity) {
|
||||
let placeholder = UIImage(named: "room_default_user", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
if let url = URL(string: item.avatarUrl) {
|
||||
avatarImageView.sd_setImage(with: url, placeholderImage: placeholder)
|
||||
} else {
|
||||
avatarImageView.image = placeholder
|
||||
}
|
||||
userLabel.text = item.userName
|
||||
backgroundColor = .clear
|
||||
}
|
||||
|
||||
@objc func agreeStageAction(sender: UIButton) {
|
||||
viewModel.respondRequest(isAgree: true, request: attendeeModel)
|
||||
}
|
||||
|
||||
@objc func disagreeStageAction(sender: UIButton) {
|
||||
viewModel.respondRequest(isAgree: false, request: attendeeModel)
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var applyText: String {
|
||||
localized("Apply to be on stage")
|
||||
}
|
||||
static var disagreeSeatText: String {
|
||||
localized("Reject")
|
||||
}
|
||||
static var agreeSeatText: String {
|
||||
localized("Agree")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
//
|
||||
// RaiseHandApplicationListView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2023/1/13.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class RaiseHandApplicationListView: UIView {
|
||||
let viewModel: RaiseHandApplicationListViewModel
|
||||
|
||||
let titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = .takeSeatApplyTitle
|
||||
label.font = UIFont.systemFont(ofSize: 16, weight: .medium)
|
||||
label.textColor = UIColor(0xD5E0F2)
|
||||
label.backgroundColor = .clear
|
||||
return label
|
||||
}()
|
||||
|
||||
let allAgreeButton : UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .regular)
|
||||
button.setTitle(.agreeAllText, for: .normal)
|
||||
button.setTitleColor(UIColor(0xFFFFFF), for: .normal)
|
||||
button.setBackgroundImage(UIColor(0x1C66E5).withAlphaComponent(0.5).trans2Image(), for: .disabled)
|
||||
button.setBackgroundImage(UIColor(0x1C66E5).trans2Image(), for: .normal)
|
||||
button.layer.cornerRadius = 6
|
||||
button.clipsToBounds = true
|
||||
button.adjustsImageWhenHighlighted = false
|
||||
return button
|
||||
}()
|
||||
|
||||
let allRejectButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .regular)
|
||||
button.setTitle(.rejectAllText, for: .normal)
|
||||
button.setTitleColor(UIColor(0xB2BBD1), for: .normal)
|
||||
button.setBackgroundImage(UIColor(0x4F586B).withAlphaComponent(0.5).trans2Image(), for: .disabled)
|
||||
button.setBackgroundImage(UIColor(0x4F586B).trans2Image(), for: .normal)
|
||||
button.layer.cornerRadius = 6
|
||||
button.clipsToBounds = true
|
||||
button.titleLabel?.adjustsFontSizeToFitWidth = true
|
||||
button.adjustsImageWhenHighlighted = false
|
||||
return button
|
||||
}()
|
||||
|
||||
let placeholderUIImageView: UIImageView = {
|
||||
let image = UIImage(named: "room_apply_placeholder", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
let imageView = UIImageView(image: image)
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let placeholderLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = .noMemberApplicationText
|
||||
label.textColor = UIColor(0xB2BBD1)
|
||||
label.font = UIFont.systemFont(ofSize: 16, weight: .regular)
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var applyTableView: UITableView = {
|
||||
let tableView = UITableView(frame: .zero, style: .plain)
|
||||
tableView.separatorStyle = .none
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.register(RaiseHandApplicationCell.self, forCellReuseIdentifier: "RaiseHandCell")
|
||||
return tableView
|
||||
}()
|
||||
|
||||
init(viewModel: RaiseHandApplicationListViewModel) {
|
||||
self.viewModel = viewModel
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
backgroundColor = UIColor(0x22262E)
|
||||
guard !isViewReady else { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(titleLabel)
|
||||
addSubview(placeholderUIImageView)
|
||||
addSubview(placeholderLabel)
|
||||
addSubview(applyTableView)
|
||||
addSubview(allRejectButton)
|
||||
addSubview(allAgreeButton)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(10.scale375Height())
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
}
|
||||
placeholderUIImageView.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.width.height.equalTo(48.scale375())
|
||||
make.centerY.equalToSuperview().offset(-30.scale375Height())
|
||||
}
|
||||
placeholderLabel.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.top.equalTo(placeholderUIImageView.snp.bottom).offset(8.scale375Height())
|
||||
make.height.equalTo(22.scale375Height())
|
||||
}
|
||||
applyTableView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(26.scale375Height())
|
||||
make.bottom.equalTo(allAgreeButton.snp.top).offset(-10.scale375Height())
|
||||
}
|
||||
allRejectButton.snp.remakeConstraints { make in
|
||||
make.leading.equalTo(applyTableView)
|
||||
make.bottom.equalToSuperview().offset(-34.scale375Height())
|
||||
make.height.equalTo(40.scale375Height())
|
||||
make.width.equalTo(167.scale375())
|
||||
}
|
||||
allAgreeButton.snp.makeConstraints { make in
|
||||
make.trailing.equalTo(applyTableView)
|
||||
make.bottom.height.width.equalTo(allRejectButton)
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
viewModel.viewResponder = self
|
||||
allAgreeButton.addTarget(self, action: #selector(allAgreeStageAction(sender:)), for: .touchUpInside)
|
||||
allRejectButton.addTarget(self, action: #selector(allRejectAction(sender:)), for: .touchUpInside)
|
||||
setupPlaceholderViewState(isShown: viewModel.isPlaceholderViewShown)
|
||||
setupApplyButtonState(isEnabled: viewModel.isApplyButtonEnabled)
|
||||
}
|
||||
|
||||
@objc func allAgreeStageAction(sender: UIButton) {
|
||||
viewModel.respondAllRequest(isAgree: true)
|
||||
}
|
||||
|
||||
@objc func allRejectAction(sender: UIButton) {
|
||||
viewModel.respondAllRequest(isAgree: false)
|
||||
}
|
||||
|
||||
private func setupPlaceholderViewState(isShown: Bool) {
|
||||
placeholderLabel.isHidden = !isShown
|
||||
placeholderUIImageView.isHidden = !isShown
|
||||
applyTableView.isHidden = isShown
|
||||
}
|
||||
|
||||
private func setupApplyButtonState(isEnabled: Bool) {
|
||||
allAgreeButton.isEnabled = isEnabled
|
||||
allRejectButton.isEnabled = isEnabled
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
|
||||
extension RaiseHandApplicationListView: UITableViewDataSource {
|
||||
internal func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return viewModel.inviteSeatList.count
|
||||
}
|
||||
}
|
||||
|
||||
extension RaiseHandApplicationListView: UITableViewDelegate {
|
||||
internal func tableView(_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let attendeeModel = viewModel.inviteSeatList[indexPath.row]
|
||||
let cell = RaiseHandApplicationCell(attendeeModel: attendeeModel, viewModel: viewModel)
|
||||
cell.selectionStyle = .none
|
||||
return cell
|
||||
}
|
||||
|
||||
internal func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return 60.scale375()
|
||||
}
|
||||
}
|
||||
|
||||
extension RaiseHandApplicationListView: RaiseHandApplicationListViewResponder {
|
||||
func updateApplyButtonState(isEnabled: Bool) {
|
||||
setupApplyButtonState(isEnabled: isEnabled)
|
||||
}
|
||||
|
||||
func updatePlaceholderViewState(isShown: Bool) {
|
||||
setupPlaceholderViewState(isShown: isShown)
|
||||
}
|
||||
|
||||
func reloadApplyListView() {
|
||||
applyTableView.reloadData()
|
||||
}
|
||||
|
||||
func makeToast(text: String) {
|
||||
RoomRouter.makeToastInCenter(toast: text, duration: 1)
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var takeSeatApplyTitle: String {
|
||||
localized("Participants apply to come on stage")
|
||||
}
|
||||
static var rejectAllText: String {
|
||||
localized("Reject all")
|
||||
}
|
||||
static var noMemberApplicationText: String {
|
||||
localized("No participants's application yet")
|
||||
}
|
||||
static var agreeAllText: String {
|
||||
localized("Agree to all")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
//
|
||||
// RaiseHandApplicationNotificationView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/5/8.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol RaiseHandApplicationNotificationViewListener: AnyObject {
|
||||
func onHidden()
|
||||
func onShown()
|
||||
}
|
||||
|
||||
class RaiseHandApplicationNotificationView: UIView {
|
||||
let viewModel: RaiseHandApplicationNotificationViewModel
|
||||
weak var delegate: RaiseHandApplicationNotificationViewListener?
|
||||
private let imageView: UIImageView = {
|
||||
let image = UIImage(named: "room_raise_hand_notification", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
return UIImageView(image: image)
|
||||
}()
|
||||
|
||||
private let label: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textAlignment = isRTL ? .right : .left
|
||||
label.font = UIFont.systemFont(ofSize: 14, weight: .regular)
|
||||
label.textColor = UIColor(0x181820)
|
||||
label.adjustsFontSizeToFitWidth = false
|
||||
return label
|
||||
}()
|
||||
|
||||
private let checkButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.backgroundColor = .clear
|
||||
button.setTitle(.checkText, for: .normal)
|
||||
button.setTitleColor(UIColor(0x1C66E5), for: .normal)
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .medium)
|
||||
button.titleLabel?.textAlignment = .center
|
||||
return button
|
||||
}()
|
||||
|
||||
init(viewModel: RaiseHandApplicationNotificationViewModel) {
|
||||
self.viewModel = viewModel
|
||||
super.init(frame: .zero)
|
||||
self.viewModel.responder = self
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - view layout
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(imageView)
|
||||
addSubview(label)
|
||||
addSubview(checkButton)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
imageView.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.leading.equalToSuperview().offset(8.scale375Height())
|
||||
make.width.height.equalTo(24.scale375())
|
||||
}
|
||||
checkButton.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.trailing.equalToSuperview().offset(-6.scale375())
|
||||
make.height.equalTo(22.scale375Height())
|
||||
make.width.equalTo(48.scale375())
|
||||
}
|
||||
label.snp.makeConstraints { make in
|
||||
make.leading.equalTo(imageView.snp.trailing).offset(10.scale375())
|
||||
make.trailing.equalTo(checkButton.snp.leading).offset(-10.scale375())
|
||||
make.centerY.equalToSuperview()
|
||||
make.height.equalTo(22.scale375Height())
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
isHidden = true
|
||||
backgroundColor = UIColor(0xFFFFFF)
|
||||
layer.cornerRadius = 6
|
||||
checkButton.addTarget(self, action: #selector(checkAction(sender:)), for: .touchUpInside)
|
||||
guard viewModel.isShownRaiseHandApplicationNotificationView else { return }
|
||||
guard let userId = viewModel.userId, let userName = viewModel.userName, let count = viewModel.applicationCount else { return }
|
||||
show(userId: userId, userName: userName, count: count)
|
||||
}
|
||||
|
||||
@objc private func checkAction(sender: UIButton) {
|
||||
hide()
|
||||
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hide), object: nil)
|
||||
viewModel.checkRaiseHandApplicationAction()
|
||||
}
|
||||
|
||||
func show(userId: String, userName: String, count: Int) {
|
||||
isHidden = false
|
||||
let nameText = userName ?? userId
|
||||
let title = count > 1 ?
|
||||
.multiApplyingOnStageText.replacingOccurrences(of: "xx", with: nameText).replacingOccurrences(of: "yy", with: String(count))
|
||||
: localizedReplace(.singleApplyingOnStageText, replace: nameText)
|
||||
label.text = title
|
||||
delegate?.onShown()
|
||||
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hide), object: nil)
|
||||
guard viewModel.delayDisappearanceTime > 0 else { return }
|
||||
perform(#selector(hide), with: nil, afterDelay: viewModel.delayDisappearanceTime)
|
||||
}
|
||||
|
||||
@objc func hide() {
|
||||
isHidden = true
|
||||
delegate?.onHidden()
|
||||
}
|
||||
|
||||
deinit {
|
||||
NSObject.cancelPreviousPerformRequests(withTarget: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension RaiseHandApplicationNotificationView: RaiseHandApplicationNotificationViewModelResponder {
|
||||
func showRaiseHandApplicationNotificationView(userId: String, userName: String, count: Int) {
|
||||
show(userId: userId, userName: userName, count: count)
|
||||
}
|
||||
|
||||
func hideRaiseHandApplicationNotificationView() {
|
||||
hide()
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var checkText: String {
|
||||
localized("Check")
|
||||
}
|
||||
static var singleApplyingOnStageText: String {
|
||||
localized("xx is applying to be on stage.")
|
||||
}
|
||||
static var multiApplyingOnStageText: String {
|
||||
localized("Including xx, yy people are applying to be on stage.")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// TopItemView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2022/12/30.
|
||||
// Copyright © 2022 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class TopItemView: UIView {
|
||||
let itemData: ButtonItemData
|
||||
|
||||
let button: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
return button
|
||||
}()
|
||||
|
||||
// MARK: - initialized function
|
||||
init(itemData: ButtonItemData) {
|
||||
self.itemData = itemData
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - view layout
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
self.layer.cornerRadius = 10
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(button)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
button.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.height.equalTo(24)
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
setupViewState(item: itemData)
|
||||
button.addTarget(self, action: #selector(clickMenuButton(sender:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
func setupViewState(item: ButtonItemData) {
|
||||
button.isSelected = item.isSelect
|
||||
button.isEnabled = item.isEnabled
|
||||
button.isHidden = item.isHidden
|
||||
if let normalImage = item.normalImage {
|
||||
button.setImage(normalImage, for: .normal)
|
||||
}
|
||||
if let selectedImage = item.selectedImage {
|
||||
button.setImage(selectedImage, for: .selected)
|
||||
}
|
||||
if let disabledImage = item.disabledImage {
|
||||
button.setImage(disabledImage, for: .disabled)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func clickMenuButton(sender: UIButton) {
|
||||
itemData.action?(sender)
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
//
|
||||
// TopView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2022/12/30.
|
||||
// Copyright © 2022 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class TopView: UIView {
|
||||
// MARK: - store property
|
||||
let viewModel: TopViewModel
|
||||
private var viewArray: [TopItemView] = []
|
||||
let backgroundImageView: UIImageView = {
|
||||
let image = UIImage(named: "room_top_background",in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
let imageView = UIImageView(image: image)
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let contentView: UIView = {
|
||||
let view = UIView()
|
||||
return view
|
||||
}()
|
||||
|
||||
let stackView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.axis = .horizontal
|
||||
view.alignment = .center
|
||||
view.spacing = 2.scale375()
|
||||
return view
|
||||
}()
|
||||
|
||||
let meetingTitleView: UIView = {
|
||||
let view = UIView()
|
||||
return view
|
||||
}()
|
||||
|
||||
let meetingNameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor(0xD5E0F2)
|
||||
label.font = UIFont(name: "PingFangSC-Medium", size: 16)
|
||||
label.textAlignment = isRTL ? .right : .left
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
return label
|
||||
}()
|
||||
|
||||
let dropDownButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
let normalIcon = UIImage(named: "room_drop_down", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
button.setImage(normalIcon, for: .normal)
|
||||
button.isEnabled = true
|
||||
return button
|
||||
}()
|
||||
|
||||
let timeLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor(0xD1D9EC)
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
label.font = UIFont(name: "PingFangSC-Medium", size: 12)
|
||||
return label
|
||||
}()
|
||||
|
||||
let exitView: UIView = {
|
||||
let view = UIView()
|
||||
return view
|
||||
}()
|
||||
|
||||
let exitImage: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.image = UIImage(named: "room_exit", in: tuiRoomKitBundle(), compatibleWith: nil)?.checkOverturn()
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let exitLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = .exitText
|
||||
label.textColor = UIColor(0xED414D)
|
||||
label.textAlignment = .right
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
label.font = UIFont(name: "PingFangSC-Medium", size: 14)
|
||||
return label
|
||||
}()
|
||||
|
||||
var menuButtons: [UIView] = []
|
||||
|
||||
// MARK: - initialized function
|
||||
init(viewModel: TopViewModel) {
|
||||
self.viewModel = viewModel
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - view layout
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
backgroundColor = UIColor(0x0F1014)
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(backgroundImageView)
|
||||
addSubview(contentView)
|
||||
contentView.addSubview(meetingTitleView)
|
||||
contentView.addSubview(stackView)
|
||||
contentView.addSubview(exitView)
|
||||
meetingTitleView.addSubview(meetingNameLabel)
|
||||
meetingTitleView.addSubview(dropDownButton)
|
||||
meetingTitleView.addSubview(timeLabel)
|
||||
exitView.addSubview(exitImage)
|
||||
exitView.addSubview(exitLabel)
|
||||
for item in viewModel.viewItems {
|
||||
let view = TopItemView(itemData: item)
|
||||
menuButtons.append(view)
|
||||
stackView.addArrangedSubview(view)
|
||||
viewArray.append(view)
|
||||
let size = item.size ?? CGSize(width: 35.scale375(), height: 40.scale375Height())
|
||||
view.snp.makeConstraints { make in
|
||||
make.height.equalTo(size.height)
|
||||
make.width.equalTo(size.width)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
updateRootViewOrientation(isLandscape: isLandscape)
|
||||
backgroundImageView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
meetingTitleView.snp.makeConstraints { make in
|
||||
make.width.lessThanOrEqualTo(180.scale375())
|
||||
make.height.equalTo(44.scale375Height())
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
stackView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
make.top.bottom.equalToSuperview()
|
||||
}
|
||||
meetingNameLabel.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview()
|
||||
make.height.equalTo(24.scale375())
|
||||
make.width.lessThanOrEqualTo(128.scale375())
|
||||
make.leading.equalToSuperview()
|
||||
}
|
||||
timeLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(meetingNameLabel.snp.bottom).offset(5)
|
||||
make.centerX.equalToSuperview()
|
||||
make.height.equalTo(20)
|
||||
}
|
||||
exitView.snp.makeConstraints { make in
|
||||
make.top.bottom.equalToSuperview()
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
make.width.equalTo(51.scale375())
|
||||
}
|
||||
exitImage.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.left.equalToSuperview()
|
||||
make.width.equalTo(20.scale375())
|
||||
make.height.equalTo(20.scale375())
|
||||
}
|
||||
exitLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.right.equalToSuperview()
|
||||
make.width.equalTo(28.scale375())
|
||||
make.height.equalTo(20.scale375())
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
let dropTap = UITapGestureRecognizer(target: self, action: #selector(dropDownAction(sender:)))
|
||||
let exitTap = UITapGestureRecognizer(target: self, action: #selector(exitAction(sender:)))
|
||||
meetingNameLabel.text = viewModel.engineManager.store.roomInfo.name
|
||||
meetingTitleView.addGestureRecognizer(dropTap)
|
||||
exitView.addGestureRecognizer(exitTap)
|
||||
viewModel.viewResponder = self
|
||||
}
|
||||
|
||||
func updateRootViewOrientation(isLandscape: Bool) {
|
||||
contentView.snp.remakeConstraints { make in
|
||||
if isLandscape {
|
||||
make.top.equalToSuperview()
|
||||
} else {
|
||||
make.top.equalToSuperview().offset(44.scale375Height())
|
||||
}
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
}
|
||||
meetingTitleView.snp.remakeConstraints { make in
|
||||
if isLandscape {
|
||||
make.width.lessThanOrEqualTo(300.scale375())
|
||||
make.height.equalTo(24.scale375Height())
|
||||
} else {
|
||||
make.width.lessThanOrEqualTo(180.scale375())
|
||||
make.height.equalTo(44.scale375Height())
|
||||
}
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
dropDownButton.snp.remakeConstraints { make in
|
||||
make.leading.equalTo(meetingNameLabel.snp.trailing).offset(2.scale375())
|
||||
make.centerY.equalTo(meetingNameLabel)
|
||||
make.width.height.equalTo(16.scale375())
|
||||
if !isLandscape {
|
||||
make.trailing.equalToSuperview()
|
||||
}
|
||||
}
|
||||
timeLabel.snp.remakeConstraints { make in
|
||||
if isLandscape {
|
||||
make.centerY.equalTo(dropDownButton)
|
||||
make.leading.equalTo(dropDownButton.snp.trailing).offset(15)
|
||||
make.trailing.equalToSuperview()
|
||||
} else {
|
||||
make.top.equalTo(meetingNameLabel.snp.bottom).offset(5)
|
||||
make.centerX.equalToSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func dropDownAction(sender: UIView) {
|
||||
viewModel.dropDownAction(sender: sender)
|
||||
}
|
||||
|
||||
@objc func exitAction(sender: UIView) {
|
||||
viewModel.exitAction(sender: sender)
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
|
||||
enum AlertAction {
|
||||
case dismissRoomAction
|
||||
case transferMasterAction
|
||||
case leaveRoomAction
|
||||
case cancelAction
|
||||
}
|
||||
|
||||
extension TopView: TopViewModelResponder {
|
||||
func updateMeetingNameLabel(_ text: String) {
|
||||
meetingNameLabel.text = text
|
||||
}
|
||||
|
||||
func updateStackView(item: ButtonItemData) {
|
||||
guard let view = viewArray.first(where: { $0.itemData.buttonType == item.buttonType }) else { return }
|
||||
view.setupViewState(item: item)
|
||||
}
|
||||
|
||||
func updateTimerLabel(text: String) {
|
||||
self.timeLabel.text = text
|
||||
}
|
||||
|
||||
#if RTCube_APPSTORE
|
||||
func showReportView() {
|
||||
let selector = NSSelectorFromString("showReportAlertWithRoomId:ownerId:")
|
||||
if responds(to: selector) {
|
||||
let roomInfo = viewModel.store.roomInfo
|
||||
perform(selector, with: roomInfo.roomId, with: roomInfo.ownerId)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var leaveRoomTitle: String {
|
||||
localized("Are you sure you want to leave the conference?")
|
||||
}
|
||||
static var destroyRoomTitle: String {
|
||||
localized("Are you sure you want to end the conference?")
|
||||
}
|
||||
static var dismissMeetingTitle: String {
|
||||
localized("If you don't want to end the conference")
|
||||
}
|
||||
static var appointNewHostText: String {
|
||||
localized("Please appoint a new host before leaving the conference")
|
||||
}
|
||||
static var leaveMeetingText: String {
|
||||
localized("Leave conference")
|
||||
}
|
||||
static var dismissMeetingText: String {
|
||||
localized("End conference")
|
||||
}
|
||||
static var cancelText: String {
|
||||
localized("Cancel")
|
||||
}
|
||||
static var exitText: String {
|
||||
localized("Exit")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
//
|
||||
// TransferMasterView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2023/2/20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class TransferMasterView: UIView {
|
||||
let viewModel: TransferMasterViewModel
|
||||
var attendeeList: [UserEntity]
|
||||
var searchArray: [UserEntity] = []
|
||||
private var isSearching: Bool = false
|
||||
|
||||
let topLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor(0xD5E0F2)
|
||||
label.font = UIFont(name: "PingFangSC-Regular", size: 16)
|
||||
label.text = .transferMasterText
|
||||
return label
|
||||
}()
|
||||
|
||||
let searchBar: UISearchBar = {
|
||||
let searchBar = UISearchBar()
|
||||
searchBar.placeholder = .searchMemberText
|
||||
searchBar.setBackgroundImage(UIColor(0x1B1E26).trans2Image(), for: .top, barMetrics: .default)
|
||||
if #available(iOS 13, *) {
|
||||
searchBar.searchTextField.textColor = UIColor(0xB2BBD1)
|
||||
searchBar.searchTextField.tintColor = UIColor(0xB2BBD1).withAlphaComponent(0.3)
|
||||
searchBar.searchTextField.layer.cornerRadius = 6
|
||||
searchBar.searchTextField.font = UIFont.systemFont(ofSize: 14, weight: .medium)
|
||||
} else {
|
||||
if let textField = searchBar.value(forKey: "searchField") as? UITextField {
|
||||
textField.textColor = UIColor(0xB2BBD1)
|
||||
textField.tintColor = UIColor(0xB2BBD1).withAlphaComponent(0.3)
|
||||
textField.layer.cornerRadius = 6
|
||||
textField.font = UIFont.systemFont(ofSize: 14, weight: .medium)
|
||||
}
|
||||
}
|
||||
return searchBar
|
||||
}()
|
||||
|
||||
let searchControl: UIControl = {
|
||||
let view = UIControl()
|
||||
view.backgroundColor = .clear
|
||||
view.isHidden = true
|
||||
return view
|
||||
}()
|
||||
|
||||
let appointMasterButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 14)
|
||||
button.setTitle(.appointAndLeaveRoomText, for: .normal)
|
||||
button.setTitleColor(UIColor(0xFFFFFF), for: .normal)
|
||||
button.setBackgroundImage(UIColor(0x006EFF).withAlphaComponent(0.2).trans2Image(), for: .disabled)
|
||||
button.setBackgroundImage(UIColor(0x006EFF).trans2Image(), for: .normal)
|
||||
button.layer.cornerRadius = 12
|
||||
button.clipsToBounds = true
|
||||
button.titleLabel?.adjustsFontSizeToFitWidth = true
|
||||
button.adjustsImageWhenHighlighted = false
|
||||
return button
|
||||
}()
|
||||
|
||||
lazy var transferMasterTableView: UITableView = {
|
||||
let tableView = UITableView(frame: .zero, style: .plain)
|
||||
tableView.separatorStyle = .none
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
tableView.backgroundColor = UIColor(0x1B1E26)
|
||||
tableView.register(TransferMasterTableCell.self, forCellReuseIdentifier: "RaiseHandCell")
|
||||
return tableView
|
||||
}()
|
||||
|
||||
init(viewModel: TransferMasterViewModel) {
|
||||
self.viewModel = viewModel
|
||||
self.attendeeList = viewModel.attendeeList
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
backgroundColor = UIColor(0x1B1E26)
|
||||
guard !isViewReady else { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(topLabel)
|
||||
addSubview(searchBar)
|
||||
addSubview(transferMasterTableView)
|
||||
addSubview(appointMasterButton)
|
||||
addSubview(searchControl)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
topLabel.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(10.scale375Height())
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
make.height.equalTo(24.scale375Height())
|
||||
}
|
||||
searchBar.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
make.height.equalTo(34.scale375Height())
|
||||
make.top.equalTo(topLabel.snp.bottom).offset(23.scale375Height())
|
||||
}
|
||||
transferMasterTableView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
make.top.equalTo(searchBar.snp.bottom).offset(10.scale375Height())
|
||||
make.bottom.equalToSuperview()
|
||||
}
|
||||
appointMasterButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-20)
|
||||
make.bottom.equalToSuperview().offset(-40 - kDeviceSafeBottomHeight)
|
||||
make.height.equalTo(50)
|
||||
make.leading.equalToSuperview().offset(20)
|
||||
}
|
||||
searchControl.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
searchBar.delegate = self
|
||||
viewModel.viewResponder = self
|
||||
appointMasterButton.addTarget(self, action: #selector(appointMasterAction(sender:)), for: .touchUpInside)
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(hideSearchControl(sender:)))
|
||||
searchControl.addGestureRecognizer(tap)
|
||||
appointMasterButton.isEnabled = viewModel.userId.count > 0
|
||||
}
|
||||
|
||||
@objc func appointMasterAction(sender: UIButton) {
|
||||
viewModel.appointMasterAction(sender: sender)
|
||||
}
|
||||
|
||||
@objc func hideSearchControl(sender: UIView) {
|
||||
if #available(iOS 13, *) {
|
||||
searchBar.searchTextField.resignFirstResponder()
|
||||
} else {
|
||||
searchBar.resignFirstResponder()
|
||||
}
|
||||
searchControl.isHidden = true
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
|
||||
extension TransferMasterView: UISearchBarDelegate {
|
||||
func searchBar(_ searchBar:UISearchBar,textDidChange searchText:String){
|
||||
let searchContentText = searchText.trimmingCharacters(in: .whitespaces)
|
||||
if searchContentText.count == 0 {
|
||||
attendeeList = viewModel.attendeeList
|
||||
transferMasterTableView.reloadData()
|
||||
isSearching = false
|
||||
} else {
|
||||
searchArray = viewModel.attendeeList.filter({ model -> Bool in
|
||||
return model.userName.contains(searchContentText)
|
||||
})
|
||||
attendeeList = searchArray
|
||||
transferMasterTableView.reloadData()
|
||||
isSearching = true
|
||||
}
|
||||
}
|
||||
|
||||
func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
|
||||
searchControl.isHidden = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension TransferMasterView: UITableViewDataSource {
|
||||
internal func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return attendeeList.count
|
||||
}
|
||||
}
|
||||
|
||||
extension TransferMasterView: UITableViewDelegate {
|
||||
internal func tableView(_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let attendeeModel = attendeeList[indexPath.row]
|
||||
let cell = TransferMasterTableCell(attendeeModel: attendeeModel, viewModel: viewModel)
|
||||
cell.selectionStyle = .none
|
||||
return cell
|
||||
}
|
||||
|
||||
internal func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
searchBar.endEditing(true)
|
||||
if #available(iOS 13, *) {
|
||||
searchBar.searchTextField.resignFirstResponder()
|
||||
} else {
|
||||
searchBar.resignFirstResponder()
|
||||
}
|
||||
viewModel.userId = attendeeList[indexPath.row].userId
|
||||
appointMasterButton.isEnabled = true
|
||||
transferMasterTableView.reloadData()
|
||||
}
|
||||
internal func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return 60.scale375Height()
|
||||
}
|
||||
}
|
||||
|
||||
extension TransferMasterView: TransferMasterViewResponder {
|
||||
func makeToast(message: String) {
|
||||
makeToast(message)
|
||||
}
|
||||
|
||||
func reloadTransferMasterTableView() {
|
||||
guard !isSearching else { return }
|
||||
attendeeList = viewModel.attendeeList
|
||||
transferMasterTableView.reloadData()
|
||||
}
|
||||
|
||||
func searchControllerChangeActive(isActive: Bool) {
|
||||
searchBar.endEditing(!isActive)
|
||||
if #available(iOS 13, *) {
|
||||
searchBar.searchTextField.resignFirstResponder()
|
||||
} else {
|
||||
searchBar.resignFirstResponder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TransferMasterTableCell: UITableViewCell {
|
||||
let attendeeModel: UserEntity
|
||||
let viewModel: TransferMasterViewModel
|
||||
|
||||
let avatarImageView: UIImageView = {
|
||||
let img = UIImageView()
|
||||
img.layer.cornerRadius = 20
|
||||
img.layer.masksToBounds = true
|
||||
return img
|
||||
}()
|
||||
|
||||
let userLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor(0xD5E0F2)
|
||||
label.backgroundColor = UIColor.clear
|
||||
label.textAlignment = isRTL ? .right : .left
|
||||
label.textAlignment = .left
|
||||
label.font = UIFont(name: "PingFangSC-Regular", size: 16)
|
||||
label.numberOfLines = 1
|
||||
return label
|
||||
}()
|
||||
|
||||
let checkMarkButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.setImage(UIImage(named: "room_check_mark", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
|
||||
button.isHidden = true
|
||||
return button
|
||||
}()
|
||||
|
||||
let downLineView : UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(0x2A2D38)
|
||||
return view
|
||||
}()
|
||||
|
||||
init(attendeeModel: UserEntity ,viewModel: TransferMasterViewModel) {
|
||||
self.attendeeModel = attendeeModel
|
||||
self.viewModel = viewModel
|
||||
super.init(style: .default, reuseIdentifier: "TransferMasterTableCell")
|
||||
}
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
contentView.addSubview(avatarImageView)
|
||||
contentView.addSubview(userLabel)
|
||||
contentView.addSubview(checkMarkButton)
|
||||
contentView.addSubview(downLineView)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
avatarImageView.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(40.scale375Height())
|
||||
make.leading.equalToSuperview()
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
checkMarkButton.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(22.scale375())
|
||||
make.trailing.equalToSuperview()
|
||||
make.centerY.equalTo(self.avatarImageView)
|
||||
}
|
||||
userLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.leading.equalTo(avatarImageView.snp.trailing).offset(10.scale375())
|
||||
make.width.equalTo(150.scale375())
|
||||
make.height.equalTo(22.scale375Height())
|
||||
}
|
||||
downLineView.snp.makeConstraints { make in
|
||||
make.leading.equalTo(userLabel)
|
||||
make.trailing.equalToSuperview()
|
||||
make.bottom.equalToSuperview()
|
||||
make.height.equalTo(1.scale375Height())
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
backgroundColor = UIColor(0x1B1E26)
|
||||
setupViewState(item: attendeeModel)
|
||||
}
|
||||
|
||||
func setupViewState(item: UserEntity) {
|
||||
let placeholder = UIImage(named: "room_default_user", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
if let url = URL(string: item.avatarUrl) {
|
||||
avatarImageView.sd_setImage(with: url, placeholderImage: placeholder)
|
||||
} else {
|
||||
avatarImageView.image = placeholder
|
||||
}
|
||||
userLabel.text = item.userName
|
||||
if viewModel.userId == attendeeModel.userId {
|
||||
checkMarkButton.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var transferMasterText: String {
|
||||
localized("Appoint a new master")
|
||||
}
|
||||
static var searchMemberText: String {
|
||||
localized("Search for participants")
|
||||
}
|
||||
static var appointAndLeaveRoomText: String {
|
||||
localized("Appoint and leave")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
//
|
||||
// UserListCell.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/5/7.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RTCRoomEngine
|
||||
import Factory
|
||||
import Combine
|
||||
|
||||
class UserListCell: UITableViewCell {
|
||||
var attendeeModel: UserEntity
|
||||
var viewModel: UserListViewModel
|
||||
|
||||
let avatarImageView: UIImageView = {
|
||||
let img = UIImageView()
|
||||
img.layer.cornerRadius = 20
|
||||
img.layer.masksToBounds = true
|
||||
return img
|
||||
}()
|
||||
|
||||
let userLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor(0xD5E0F2)
|
||||
label.backgroundColor = UIColor.clear
|
||||
label.textAlignment = isRTL ? .right : .left
|
||||
label.textAlignment = .left
|
||||
label.font = UIFont(name: "PingFangSC-Regular", size: 16)
|
||||
label.numberOfLines = 1
|
||||
return label
|
||||
}()
|
||||
|
||||
let roleImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let roleLabel: UILabel = {
|
||||
let label = UILabel ()
|
||||
label.font = UIFont(name: "PingFangSC-Regular", size: 12)
|
||||
label.backgroundColor = UIColor.clear
|
||||
label.textColor = UIColor(0x4791FF)
|
||||
return label
|
||||
}()
|
||||
|
||||
let muteAudioButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setImage(UIImage(named: "room_unMute_audio", in: tuiRoomKitBundle(), compatibleWith: nil)?.checkOverturn(), for: .normal)
|
||||
button.setImage(UIImage(named: "room_mute_audio_red", in: tuiRoomKitBundle(), compatibleWith: nil)?.checkOverturn(), for: .selected)
|
||||
return button
|
||||
}()
|
||||
|
||||
let muteVideoButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.setImage(UIImage(named: "room_unMute_video", in: tuiRoomKitBundle(), compatibleWith: nil)?.checkOverturn(), for: .normal)
|
||||
button.setImage(UIImage(named: "room_mute_video_red", in: tuiRoomKitBundle(), compatibleWith: nil)?.checkOverturn(), for: .selected)
|
||||
return button
|
||||
}()
|
||||
|
||||
let inviteStageButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.backgroundColor = UIColor(0x0565FA)
|
||||
button.layer.cornerRadius = 6
|
||||
button.setTitle(.inviteSeatText, for: .normal)
|
||||
button.setTitleColor(UIColor(0xFFFFFF), for: .normal)
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 12, weight: .regular)
|
||||
button.isHidden = true
|
||||
return button
|
||||
}()
|
||||
|
||||
let callButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.backgroundColor = UIColor(0x6B758A)
|
||||
button.layer.cornerRadius = 6
|
||||
button.setTitle(.callText, for: .normal)
|
||||
button.setTitleColor(UIColor(0xB2BBD1), for: .normal)
|
||||
button.titleLabel?.font = UIFont(name: "PingFangSC-Regular", size: 12)
|
||||
button.isHidden = true
|
||||
return button
|
||||
}()
|
||||
|
||||
let callingLabel: UILabel = {
|
||||
let label = UILabel ()
|
||||
label.font = UIFont(name: "PingFangSC-Regular", size: 14)
|
||||
label.backgroundColor = UIColor.clear
|
||||
label.textColor = UIColor(0xD5E0F2)
|
||||
label.text = .callingText
|
||||
label.isHidden = true
|
||||
return label
|
||||
}()
|
||||
|
||||
let notJoiningLabel: UILabel = {
|
||||
let label = UILabel ()
|
||||
label.font = UIFont(name: "PingFangSC-Regular", size: 14)
|
||||
label.backgroundColor = UIColor.clear
|
||||
label.textAlignment = .right
|
||||
label.textColor = UIColor(0xD5E0F2)
|
||||
label.text = .notJoinNowText
|
||||
label.isHidden = true
|
||||
return label
|
||||
}()
|
||||
|
||||
let downLineView : UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(0x4F586B,alpha: 0.3)
|
||||
return view
|
||||
}()
|
||||
|
||||
init(attendeeModel: UserEntity ,viewModel: UserListViewModel) {
|
||||
self.attendeeModel = attendeeModel
|
||||
self.viewModel = viewModel
|
||||
super.init(style: .default, reuseIdentifier: "UserListCell")
|
||||
}
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
isViewReady = true
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
contentView.addSubview(avatarImageView)
|
||||
contentView.addSubview(userLabel)
|
||||
contentView.addSubview(roleImageView)
|
||||
contentView.addSubview(roleLabel)
|
||||
contentView.addSubview(muteAudioButton)
|
||||
contentView.addSubview(muteVideoButton)
|
||||
contentView.addSubview(inviteStageButton)
|
||||
contentView.addSubview(downLineView)
|
||||
contentView.addSubview(callButton)
|
||||
contentView.addSubview(callingLabel)
|
||||
contentView.addSubview(notJoiningLabel)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
avatarImageView.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(40)
|
||||
make.leading.equalToSuperview()
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
muteVideoButton.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(20.scale375())
|
||||
make.trailing.equalToSuperview()
|
||||
make.centerY.equalTo(self.avatarImageView)
|
||||
}
|
||||
muteAudioButton.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(20.scale375())
|
||||
make.trailing.equalTo(self.muteVideoButton.snp.leading).offset(-20.scale375())
|
||||
make.centerY.equalTo(self.avatarImageView)
|
||||
}
|
||||
inviteStageButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview()
|
||||
make.centerY.equalTo(self.avatarImageView)
|
||||
make.width.equalTo(80.scale375())
|
||||
make.height.equalTo(30.scale375Height())
|
||||
}
|
||||
userLabel.snp.makeConstraints { make in
|
||||
if attendeeModel.userRole == .generalUser {
|
||||
make.centerY.equalToSuperview()
|
||||
} else {
|
||||
make.top.equalToSuperview().offset(10.scale375Height())
|
||||
}
|
||||
make.leading.equalTo(avatarImageView.snp.trailing).offset(12.scale375())
|
||||
make.width.equalTo(150.scale375())
|
||||
make.height.equalTo(22.scale375())
|
||||
}
|
||||
roleImageView.snp.makeConstraints { make in
|
||||
make.top.equalTo(userLabel.snp.bottom).offset(2.scale375Height())
|
||||
make.leading.equalTo(avatarImageView.snp.trailing).offset(12.scale375())
|
||||
make.width.height.equalTo(14.scale375())
|
||||
}
|
||||
roleLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(roleImageView)
|
||||
make.leading.equalTo(roleImageView.snp.trailing).offset(2.scale375())
|
||||
make.trailing.equalTo(81.scale375())
|
||||
make.height.equalTo(16.scale375())
|
||||
}
|
||||
callButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview()
|
||||
make.centerY.equalTo(self.avatarImageView)
|
||||
make.width.equalTo(48.scale375())
|
||||
make.height.equalTo(28.scale375Height())
|
||||
}
|
||||
callingLabel.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview()
|
||||
make.centerY.equalTo(self.avatarImageView)
|
||||
make.width.equalTo(60.scale375())
|
||||
make.height.equalTo(28.scale375Height())
|
||||
}
|
||||
notJoiningLabel.snp.makeConstraints{ make in
|
||||
make.trailing.equalTo(callButton.snp.leading).offset(-12.scale375())
|
||||
make.centerY.equalTo(self.avatarImageView)
|
||||
make.width.equalTo(120.scale375())
|
||||
make.height.equalTo(28.scale375Height())
|
||||
}
|
||||
downLineView.snp.makeConstraints { make in
|
||||
make.leading.equalTo(userLabel)
|
||||
make.trailing.equalToSuperview()
|
||||
make.bottom.equalToSuperview()
|
||||
make.height.equalTo(1.scale375())
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
backgroundColor = UIColor(0x17181F)
|
||||
setupViewState(item: attendeeModel)
|
||||
inviteStageButton.addTarget(self, action: #selector(inviteStageAction(sender:)), for: .touchUpInside)
|
||||
muteAudioButton.addTarget(self, action: #selector(showUserManageAction(sender:)), for: .touchUpInside)
|
||||
muteVideoButton.addTarget(self, action: #selector(showUserManageAction(sender:)), for: .touchUpInside)
|
||||
callButton.addTarget(self, action: #selector(inviteEnterAction(sender:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
func setupViewState(item: UserEntity) {
|
||||
let placeholder = UIImage(named: "room_default_user", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
if let url = URL(string: item.avatarUrl) {
|
||||
avatarImageView.sd_setImage(with: url, placeholderImage: placeholder)
|
||||
} else {
|
||||
avatarImageView.image = placeholder
|
||||
}
|
||||
if item.userId == viewModel.currentUser.userId {
|
||||
userLabel.text = item.userName + "(" + .meText + ")"
|
||||
} else {
|
||||
userLabel.text = item.userName
|
||||
}
|
||||
switch item.userRole {
|
||||
case .roomOwner:
|
||||
roleImageView.image = UIImage(named: "room_role_owner", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
roleLabel.text = .ownerText
|
||||
case .administrator:
|
||||
roleImageView.image = UIImage(named: "room_role_administrator", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
roleLabel.text = .administratorText
|
||||
default: break
|
||||
}
|
||||
roleImageView.isHidden = item.userRole == .generalUser
|
||||
roleLabel.isHidden = item.userRole == .generalUser
|
||||
muteAudioButton.isSelected = !item.hasAudioStream
|
||||
muteVideoButton.isSelected = !item.hasVideoStream
|
||||
if viewModel.roomInfo.isSeatEnabled {
|
||||
muteAudioButton.isHidden = !attendeeModel.isOnSeat
|
||||
muteVideoButton.isHidden = !attendeeModel.isOnSeat
|
||||
if viewModel.checkSelfInviteAbility(invitee: attendeeModel) {
|
||||
inviteStageButton.isHidden = attendeeModel.isOnSeat
|
||||
} else {
|
||||
inviteStageButton.isHidden = true
|
||||
}
|
||||
}
|
||||
setupCallingViewState(item: item)
|
||||
}
|
||||
|
||||
private func setupCallingViewState(item: UserEntity) {
|
||||
if let index = viewModel.invitationList.firstIndex(where: { $0.invitee.userId == item.userId }) {
|
||||
let invitation = viewModel.invitationList[index]
|
||||
muteAudioButton.isHidden = true
|
||||
muteVideoButton.isHidden = true
|
||||
inviteStageButton.isHidden = true
|
||||
let isPending = invitation.status == .pending
|
||||
callButton.isHidden = isPending
|
||||
callingLabel.isHidden = !isPending
|
||||
notJoiningLabel.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
func showNotJoiningLabel() {
|
||||
self.notJoiningLabel.isHidden = false
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
|
||||
self.notJoiningLabel.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
@objc func inviteStageAction(sender: UIButton) {
|
||||
viewModel.userId = attendeeModel.userId
|
||||
viewModel.inviteSeatAction(sender: sender)
|
||||
}
|
||||
|
||||
@objc func showUserManageAction(sender: UIButton) {
|
||||
viewModel.showUserManageViewAction(userId: attendeeModel.userId, userName: attendeeModel.userName)
|
||||
}
|
||||
|
||||
@objc func inviteEnterAction(sender: UIButton) {
|
||||
self.conferenceStore.dispatch(action: ConferenceInvitationActions.inviteUsers(payload: (viewModel.roomInfo.roomId, [attendeeModel.userId])))
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
|
||||
@Injected(\.conferenceStore) private var conferenceStore
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var inviteSeatText: String {
|
||||
localized("Invite to stage")
|
||||
}
|
||||
static var meText: String {
|
||||
localized("Me")
|
||||
}
|
||||
static var ownerText: String {
|
||||
localized("Host")
|
||||
}
|
||||
static var administratorText: String {
|
||||
localized("Administrator")
|
||||
}
|
||||
static var callText: String {
|
||||
localized("Call")
|
||||
}
|
||||
static var callingText: String {
|
||||
localized("Calling...")
|
||||
}
|
||||
static var notJoinNowText: String {
|
||||
localized("Not joining for now")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
//
|
||||
// UserListManagerView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2023/1/5.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class UserListManagerView: UIView {
|
||||
var viewModel: UserListManagerViewModel
|
||||
private var isViewReady: Bool = false
|
||||
private var viewArray: [ButtonItemView] = []
|
||||
private var currentLandscape: Bool = isLandscape
|
||||
|
||||
let contentView: UIView = {
|
||||
let view = UIView(frame: .zero)
|
||||
view.backgroundColor = UIColor(0x22262E)
|
||||
view.layer.cornerRadius = 12
|
||||
return view
|
||||
}()
|
||||
|
||||
let dropArrowButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setImage(UIImage(named: "room_drop_arrow", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
|
||||
button.contentEdgeInsets = UIEdgeInsets(top: 12.scale375Height(), left: 20.scale375(), bottom: 12.scale375Height(), right: 20.scale375())
|
||||
return button
|
||||
}()
|
||||
|
||||
let rightArrowButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setImage(UIImage(named: "room_right_arrow", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
|
||||
button.contentEdgeInsets = UIEdgeInsets(top: 20.scale375Height(), left: 12.scale375(), bottom: 20.scale375Height(), right: 12.scale375())
|
||||
return button
|
||||
}()
|
||||
|
||||
let scrollView: UIScrollView = {
|
||||
let view = UIScrollView()
|
||||
return view
|
||||
}()
|
||||
|
||||
let avatarImageView: UIImageView = {
|
||||
let img = UIImageView()
|
||||
img.layer.cornerRadius = 20
|
||||
img.layer.masksToBounds = true
|
||||
return img
|
||||
}()
|
||||
|
||||
let userLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor(0xD5E0F2)
|
||||
label.textAlignment = isRTL ? .right : .left
|
||||
label.font = UIFont(name: "PingFangSC-Regular", size: 16)
|
||||
label.numberOfLines = 1
|
||||
return label
|
||||
}()
|
||||
|
||||
let headView: UIView = {
|
||||
let view = UIView()
|
||||
return view
|
||||
}()
|
||||
|
||||
let stackView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.axis = .vertical
|
||||
view.alignment = .center
|
||||
view.distribution = .equalSpacing
|
||||
view.spacing = 0
|
||||
return view
|
||||
}()
|
||||
|
||||
let backBlockView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(0x17181F)
|
||||
view.alpha = 0.9
|
||||
return view
|
||||
}()
|
||||
|
||||
init(viewModel: UserListManagerViewModel) {
|
||||
self.viewModel = viewModel
|
||||
super.init(frame: .zero)
|
||||
contentView.transform = CGAffineTransform(translationX: 0, y: kScreenHeight)
|
||||
alpha = 0
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
guard currentLandscape != isLandscape else { return }
|
||||
setupViewOrientation(isLandscape: isLandscape)
|
||||
currentLandscape = isLandscape
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
addSubview(backBlockView)
|
||||
addSubview(contentView)
|
||||
contentView.addSubview(dropArrowButton)
|
||||
contentView.addSubview(rightArrowButton)
|
||||
contentView.addSubview(scrollView)
|
||||
scrollView.addSubview(headView)
|
||||
scrollView.addSubview(stackView)
|
||||
headView.addSubview(avatarImageView)
|
||||
headView.addSubview(userLabel)
|
||||
setupStackView()
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
setupViewOrientation(isLandscape: isLandscape)
|
||||
headView.snp.makeConstraints { make in
|
||||
make.top.leading.equalToSuperview()
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
make.height.equalTo(40.scale375())
|
||||
}
|
||||
avatarImageView.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.leading.equalToSuperview()
|
||||
make.width.height.equalTo(40.scale375())
|
||||
}
|
||||
userLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.leading.equalTo(avatarImageView.snp.trailing).offset(10.scale375())
|
||||
make.trailing.equalToSuperview()
|
||||
make.height.equalTo(22.scale375())
|
||||
}
|
||||
stackView.snp.makeConstraints { make in
|
||||
make.top.equalTo(headView.snp.bottom).offset(20.scale375())
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.bottom.equalToSuperview()
|
||||
make.width.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupViewOrientation(isLandscape: Bool) {
|
||||
backBlockView.snp.remakeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
contentView.snp.remakeConstraints { make in
|
||||
if isLandscape {
|
||||
make.height.equalToSuperview()
|
||||
} else {
|
||||
make.height.equalTo(500.scale375())
|
||||
}
|
||||
make.bottom.leading.trailing.equalToSuperview()
|
||||
}
|
||||
dropArrowButton.snp.remakeConstraints { make in
|
||||
make.height.equalTo(isLandscape ? 0 : 43.scale375())
|
||||
make.top.centerX.equalToSuperview()
|
||||
}
|
||||
rightArrowButton.snp.remakeConstraints { make in
|
||||
make.width.equalTo(isLandscape ? 27.scale375() : 0)
|
||||
make.leading.centerY.equalToSuperview()
|
||||
}
|
||||
scrollView.snp.remakeConstraints { make in
|
||||
make.top.equalTo(dropArrowButton.snp.bottom).offset(10.scale375())
|
||||
if isLandscape {
|
||||
make.leading.equalTo(rightArrowButton.snp.trailing).offset(5.scale375())
|
||||
} else {
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
}
|
||||
make.bottom.equalToSuperview()
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
viewModel.viewResponder = self
|
||||
setupViewState()
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(dismiss))
|
||||
backBlockView.addGestureRecognizer(tap)
|
||||
dropArrowButton.addTarget(self, action: #selector(dismiss), for: .touchUpInside)
|
||||
rightArrowButton.addTarget(self, action: #selector(dismiss), for: .touchUpInside)
|
||||
}
|
||||
|
||||
private func setupStackView() {
|
||||
for item in viewModel.userListManagerItems {
|
||||
let view = ButtonItemView(itemData: item)
|
||||
viewArray.append(view)
|
||||
stackView.addArrangedSubview(view)
|
||||
view.snp.makeConstraints { make in
|
||||
make.height.equalTo(53.scale375())
|
||||
make.width.equalToSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupViewState() {
|
||||
let placeholder = UIImage(named: "room_default_user", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
guard let attendeeModel = viewModel.attendeeList.first(where: { $0.userId == viewModel.selectUserId }) else { return }
|
||||
if let url = URL(string: attendeeModel.avatarUrl) {
|
||||
avatarImageView.sd_setImage(with: url, placeholderImage: placeholder)
|
||||
} else {
|
||||
avatarImageView.image = placeholder
|
||||
}
|
||||
if attendeeModel.userId == viewModel.currentUser.userId {
|
||||
userLabel.text = attendeeModel.userName + "(" + .meText + ")"
|
||||
} else {
|
||||
userLabel.text = attendeeModel.userName
|
||||
}
|
||||
}
|
||||
|
||||
func updateStackView(items:[ButtonItemData]) {
|
||||
for view in viewArray {
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
viewArray.removeAll()
|
||||
for item in items {
|
||||
let view = ButtonItemView(itemData: item)
|
||||
viewArray.append(view)
|
||||
stackView.addArrangedSubview(view)
|
||||
view.snp.makeConstraints { make in
|
||||
make.height.equalTo(53.scale375())
|
||||
make.width.equalToSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func show(rootView: UIView) {
|
||||
rootView.addSubview(self)
|
||||
self.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
setupViewOrientation(isLandscape: isLandscape)
|
||||
UIView.animate(withDuration: 0.3) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.alpha = 1
|
||||
self.contentView.transform = .identity
|
||||
}
|
||||
}
|
||||
|
||||
@objc func dismiss() {
|
||||
UIView.animate(withDuration: 0.3) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.alpha = 0
|
||||
self.contentView.transform = CGAffineTransform(translationX: 0, y: kScreenHeight)
|
||||
} completion: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func backBlockAction(sender: RoomInfoView) {
|
||||
viewModel.backBlockAction(sender: self)
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
|
||||
extension UserListManagerView: UserListManagerViewEventResponder {
|
||||
func showAlert(title: String?, message: String?, sureTitle: String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?) {
|
||||
RoomRouter.presentAlert(title: title, message: message, sureTitle: sureTitle, declineTitle: declineTitle, sureBlock: sureBlock, declineBlock: declineBlock)
|
||||
}
|
||||
|
||||
func updateUI(item: ButtonItemData) {
|
||||
guard let view = viewArray.first(where: { $0.itemData.buttonType == item.buttonType }) else { return }
|
||||
view.setupViewState(item: item)
|
||||
}
|
||||
|
||||
func addStackView(item: ButtonItemData, index: Int?) {
|
||||
let view = ButtonItemView(itemData: item)
|
||||
if let index = index, viewArray.count > index + 1 {
|
||||
viewArray.insert(view, at: index)
|
||||
stackView.insertArrangedSubview(view, at: index)
|
||||
} else {
|
||||
viewArray.append(view)
|
||||
stackView.addArrangedSubview(view)
|
||||
}
|
||||
view.snp.makeConstraints { make in
|
||||
make.height.equalTo(53.scale375())
|
||||
make.width.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
func removeStackView(itemType: ButtonItemData.ButtonType) {
|
||||
let views = viewArray.filter({ view in
|
||||
view.itemData.buttonType == itemType
|
||||
})
|
||||
views.forEach { view in
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
viewArray.removeAll(where: { $0.itemData.buttonType == itemType })
|
||||
}
|
||||
|
||||
func dismissView() {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
func makeToast(text : String) {
|
||||
RoomRouter.makeToastInCenter(toast: text, duration: 0.5)
|
||||
}
|
||||
|
||||
func setUserListManagerViewHidden(isHidden: Bool) {
|
||||
self.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var meText: String {
|
||||
localized("Me")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,631 @@
|
||||
//
|
||||
// UserListView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2023/1/4.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Factory
|
||||
import Combine
|
||||
import RTCRoomEngine
|
||||
|
||||
class UserListView: UIView {
|
||||
let viewModel: UserListViewModel
|
||||
|
||||
let memberLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor(0xD5E0F2)
|
||||
label.font = UIFont(name: "PingFangSC-Regular", size: 16)
|
||||
return label
|
||||
}()
|
||||
|
||||
let searchBar: UISearchBar = {
|
||||
let searchBar = UISearchBar()
|
||||
searchBar.placeholder = .searchMemberText
|
||||
searchBar.setBackgroundImage(UIColor(0x17181F).trans2Image(), for: .top, barMetrics: .default)
|
||||
if #available(iOS 13, *) {
|
||||
searchBar.searchTextField.textColor = UIColor(0xB2BBD1)
|
||||
searchBar.searchTextField.tintColor = UIColor(0xB2BBD1).withAlphaComponent(0.3)
|
||||
searchBar.searchTextField.layer.cornerRadius = 6
|
||||
searchBar.searchTextField.font = UIFont.systemFont(ofSize: 14, weight: .medium)
|
||||
} else {
|
||||
if let textField = searchBar.value(forKey: "searchField") as? UITextField {
|
||||
textField.textColor = UIColor(0xB2BBD1)
|
||||
textField.tintColor = UIColor(0xB2BBD1).withAlphaComponent(0.3)
|
||||
textField.layer.cornerRadius = 6
|
||||
textField.font = UIFont.systemFont(ofSize: 14, weight: .medium)
|
||||
}
|
||||
}
|
||||
|
||||
return searchBar
|
||||
}()
|
||||
|
||||
let searchControl: UIControl = {
|
||||
let view = UIControl()
|
||||
view.backgroundColor = .clear
|
||||
view.isHidden = true
|
||||
return view
|
||||
}()
|
||||
|
||||
let listStateView: UIView = {
|
||||
let view = UIView(frame: .zero)
|
||||
view.backgroundColor = UIColor(0x4F586B).withAlphaComponent(0.3)
|
||||
view.layer.cornerRadius = 6
|
||||
return view
|
||||
}()
|
||||
|
||||
let haveEnteredButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.setTitleColor(UIColor(0xD5E0F2), for: .normal)
|
||||
button.titleLabel?.font = UIFont(name: "PingFangSC-Regular", size: 14)
|
||||
button.setBackgroundImage(UIColor(0xD5E0F2).withAlphaComponent(0.3).trans2Image(), for: .selected)
|
||||
button.setBackgroundImage(UIColor.clear.trans2Image(), for: .normal)
|
||||
button.layer.cornerRadius = 6
|
||||
button.clipsToBounds = true
|
||||
button.isSelected = true
|
||||
return button
|
||||
}()
|
||||
|
||||
let onStageButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.setTitleColor(UIColor(0xD5E0F2), for: .normal)
|
||||
button.titleLabel?.font = UIFont(name: "PingFangSC-Regular", size: 14)
|
||||
button.setBackgroundImage(UIColor(0xD5E0F2).withAlphaComponent(0.3).trans2Image(), for: .selected)
|
||||
button.setBackgroundImage(UIColor.clear.trans2Image(), for: .normal)
|
||||
button.layer.cornerRadius = 6
|
||||
button.clipsToBounds = true
|
||||
button.isSelected = true
|
||||
return button
|
||||
}()
|
||||
|
||||
let offStageButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.setTitleColor(UIColor(0xD5E0F2), for: .normal)
|
||||
button.titleLabel?.font = UIFont(name: "PingFangSC-Regular", size: 14)
|
||||
button.setBackgroundImage(UIColor(0xD5E0F2).withAlphaComponent(0.3).trans2Image(), for: .selected)
|
||||
button.setBackgroundImage(UIColor.clear.trans2Image(), for: .normal)
|
||||
button.layer.cornerRadius = 6
|
||||
button.clipsToBounds = true
|
||||
button.isSelected = false
|
||||
return button
|
||||
}()
|
||||
|
||||
let invatationListButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.setTitleColor(UIColor(0xD5E0F2), for: .normal)
|
||||
button.titleLabel?.font = UIFont(name: "PingFangSC-Regular", size: 14)
|
||||
button.setBackgroundImage(UIColor(0xD5E0F2).withAlphaComponent(0.3).trans2Image(), for: .selected)
|
||||
button.setBackgroundImage(UIColor.clear.trans2Image(), for: .normal)
|
||||
button.layer.cornerRadius = 6
|
||||
button.clipsToBounds = true
|
||||
button.isSelected = false
|
||||
return button
|
||||
}()
|
||||
|
||||
let muteAllAudioButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 14)
|
||||
button.setTitle(.allMuteAudioText, for: .normal)
|
||||
button.setTitleColor(UIColor(0xB2BBD1), for: .normal)
|
||||
button.setTitle(.allUnMuteAudioText, for: .selected)
|
||||
button.setTitleColor(UIColor(0xF2504B), for: .selected)
|
||||
button.setTitle(.allUnMuteAudioText, for: [.selected, .highlighted])
|
||||
button.setTitleColor(UIColor(0xF2504B), for: [.selected, .highlighted])
|
||||
button.backgroundColor = UIColor(0x4F586B, alpha: 0.3)
|
||||
button.layer.cornerRadius = 6
|
||||
button.clipsToBounds = true
|
||||
button.titleLabel?.adjustsFontSizeToFitWidth = true
|
||||
return button
|
||||
}()
|
||||
|
||||
let muteAllVideoButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 14)
|
||||
button.setTitle(.allMuteVideoText, for: .normal)
|
||||
button.setTitleColor(UIColor(0xB2BBD1), for: .normal)
|
||||
button.setTitle(.allUnMuteVideoText, for: .selected)
|
||||
button.setTitleColor(UIColor(0xF2504B), for: .selected)
|
||||
button.setTitle(.allUnMuteVideoText, for: [.selected, .highlighted])
|
||||
button.setTitleColor(UIColor(0xF2504B), for: [.selected, .highlighted])
|
||||
button.backgroundColor = UIColor(0x4F586B, alpha: 0.3)
|
||||
button.layer.cornerRadius = 6
|
||||
button.clipsToBounds = true
|
||||
button.titleLabel?.adjustsFontSizeToFitWidth = true
|
||||
return button
|
||||
}()
|
||||
|
||||
let moreFunctionButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 14)
|
||||
button.setTitle(.moreText, for: .normal)
|
||||
button.setTitleColor(UIColor(0xB2BBD1), for: .normal)
|
||||
button.backgroundColor = UIColor(0x4F586B, alpha: 0.3)
|
||||
button.layer.cornerRadius = 6
|
||||
button.clipsToBounds = true
|
||||
button.titleLabel?.adjustsFontSizeToFitWidth = true
|
||||
return button
|
||||
}()
|
||||
|
||||
let callEveryoneButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 14)
|
||||
button.setTitle(.callEveryoneText, for: .normal)
|
||||
button.setTitleColor(UIColor(0xB2BBD1), for: .normal)
|
||||
button.backgroundColor = UIColor(0x4F586B)
|
||||
button.layer.cornerRadius = 6
|
||||
button.clipsToBounds = true
|
||||
button.isHidden = true
|
||||
return button
|
||||
}()
|
||||
|
||||
let bottomControlView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(0x17181F)
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var userListTableView: UITableView = {
|
||||
let tableView = UITableView(frame: .zero, style: .plain)
|
||||
tableView.separatorStyle = .none
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
tableView.backgroundColor = UIColor(0x17181F)
|
||||
tableView.register(UserListCell.self, forCellReuseIdentifier: "UserListCell")
|
||||
return tableView
|
||||
}()
|
||||
|
||||
lazy var raiseHandNotificationView: RaiseHandApplicationNotificationView = {
|
||||
let viewModel = RaiseHandApplicationNotificationViewModel()
|
||||
viewModel.delayDisappearanceTime = 0
|
||||
let applicationNotificationView = RaiseHandApplicationNotificationView(viewModel: viewModel)
|
||||
applicationNotificationView.delegate = self.viewModel
|
||||
return applicationNotificationView
|
||||
}()
|
||||
|
||||
private lazy var invitationListPublisher = {
|
||||
conferenceStore.select(ConferenceInvitationSelectors.getInvitationList)
|
||||
}()
|
||||
|
||||
var cancellableSet = Set<AnyCancellable>()
|
||||
|
||||
init(viewModel: UserListViewModel) {
|
||||
self.viewModel = viewModel
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
isViewReady = true
|
||||
backgroundColor = UIColor(0x17181F)
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(memberLabel)
|
||||
addSubview(searchBar)
|
||||
addSubview(listStateView)
|
||||
listStateView.addSubview(haveEnteredButton)
|
||||
listStateView.addSubview(onStageButton)
|
||||
listStateView.addSubview(offStageButton)
|
||||
listStateView.addSubview(invatationListButton)
|
||||
addSubview(raiseHandNotificationView)
|
||||
addSubview(userListTableView)
|
||||
addSubview(bottomControlView)
|
||||
bottomControlView.addSubview(muteAllAudioButton)
|
||||
bottomControlView.addSubview(muteAllVideoButton)
|
||||
bottomControlView.addSubview(moreFunctionButton)
|
||||
addSubview(callEveryoneButton)
|
||||
addSubview(searchControl)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
memberLabel.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(10.scale375Height())
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
make.height.equalTo(24.scale375Height())
|
||||
}
|
||||
searchBar.snp.makeConstraints { make in
|
||||
make.top.equalTo(memberLabel.snp.bottom).offset(18.scale375Height())
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
make.height.equalTo(36.scale375Height())
|
||||
}
|
||||
if #available(iOS 13.0, *) {
|
||||
searchBar.searchTextField.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
} else {
|
||||
if let searchField = searchBar.value(forKey: "searchField") as? UITextField {
|
||||
searchField.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
listStateView.snp.makeConstraints { make in
|
||||
make.top.equalTo(searchBar.snp.bottom).offset(15.scale375Height())
|
||||
make.leading.trailing.equalTo(searchBar)
|
||||
make.height.equalTo(36.scale375Height())
|
||||
}
|
||||
setupListStateView()
|
||||
bottomControlView.snp.makeConstraints { make in
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
make.height.equalTo(84.scale375Height())
|
||||
}
|
||||
callEveryoneButton.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
make.bottom.equalToSuperview().offset(-34.scale375())
|
||||
make.height.equalTo(40.scale375Height())
|
||||
}
|
||||
raiseHandNotificationView.snp.makeConstraints { make in
|
||||
make.top.equalTo(listStateView.snp.bottom).offset(10.scale375Height())
|
||||
make.leading.equalToSuperview().offset(8.scale375())
|
||||
make.trailing.equalToSuperview().offset(-8.scale375())
|
||||
make.height.equalTo(40.scale375Height())
|
||||
}
|
||||
setupUserListTableView()
|
||||
muteAllAudioButton.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(10.scale375Height())
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
make.width.equalTo(108.scale375())
|
||||
make.height.equalTo(40.scale375())
|
||||
}
|
||||
muteAllVideoButton.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(10.scale375Height())
|
||||
make.leading.equalToSuperview().offset(133.scale375())
|
||||
make.width.equalTo(108.scale375())
|
||||
make.height.equalTo(40.scale375())
|
||||
}
|
||||
moreFunctionButton.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(10.scale375Height())
|
||||
make.leading.equalToSuperview().offset(250.scale375())
|
||||
make.width.equalTo(108.scale375())
|
||||
make.height.equalTo(40.scale375())
|
||||
}
|
||||
searchControl.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupUserListTableView() {
|
||||
guard userListTableView.superview != nil else { return }
|
||||
userListTableView.snp.remakeConstraints { make in
|
||||
let aboveView = viewModel.isShownNotificationView ? raiseHandNotificationView : listStateView
|
||||
let bottomView = viewModel.userListType == .notInRoomUsers ? callEveryoneButton : bottomControlView
|
||||
make.top.equalTo(aboveView.snp.bottom).offset(15.scale375Height())
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
make.bottom.equalTo(bottomView.snp.top)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupListStateView() {
|
||||
if viewModel.isSeatEnabled {
|
||||
onStageButton.snp.remakeConstraints { make in
|
||||
make.left.centerY.equalToSuperview()
|
||||
make.height.equalTo(32.scale375Height())
|
||||
make.width.equalToSuperview().multipliedBy(1.0 / 3.0)
|
||||
}
|
||||
offStageButton.snp.remakeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.leading.equalTo(onStageButton.snp.trailing)
|
||||
make.height.equalTo(32.scale375Height())
|
||||
make.width.equalToSuperview().multipliedBy(1.0 / 3.0)
|
||||
}
|
||||
invatationListButton.snp.remakeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.leading.equalTo(offStageButton.snp.trailing)
|
||||
make.height.equalTo(32.scale375Height())
|
||||
make.width.equalToSuperview().multipliedBy(1.0 / 3.0)
|
||||
}
|
||||
} else {
|
||||
haveEnteredButton.snp.remakeConstraints { make in
|
||||
make.left.centerY.equalToSuperview()
|
||||
make.height.equalTo(32.scale375Height())
|
||||
make.width.equalToSuperview().multipliedBy(0.5)
|
||||
}
|
||||
invatationListButton.snp.remakeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.leading.equalTo(haveEnteredButton.snp.trailing)
|
||||
make.height.equalTo(32.scale375Height())
|
||||
make.width.equalToSuperview().multipliedBy(0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
viewModel.viewResponder = self
|
||||
setupViewState()
|
||||
searchBar.delegate = self
|
||||
muteAllAudioButton.addTarget(self, action: #selector(muteAllAudioAction), for: .touchUpInside)
|
||||
muteAllVideoButton.addTarget(self, action: #selector(muteAllVideoAction), for: .touchUpInside)
|
||||
moreFunctionButton.addTarget(self, action: #selector(moreFunctionAction), for: .touchUpInside)
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(hideSearchControl(sender:)))
|
||||
searchControl.addGestureRecognizer(tap)
|
||||
haveEnteredButton.addTarget(self, action: #selector(selectAllUserAction(sender:)), for: .touchUpInside)
|
||||
onStageButton.addTarget(self, action: #selector(selectOnStageAction(sender:)), for: .touchUpInside)
|
||||
offStageButton.addTarget(self, action: #selector(selectOffStageAction(sender:)), for: .touchUpInside)
|
||||
invatationListButton.addTarget(self, action: #selector(selectInvitationListAction(sender:)), for: .touchUpInside)
|
||||
callEveryoneButton.addTarget(self, action: #selector(callEveryoneAction(sender:)), for: .touchUpInside)
|
||||
invitationListPublisher
|
||||
.receive(on: DispatchQueue.mainQueue)
|
||||
.sink { [weak self] invitationList in
|
||||
guard let self = self else { return }
|
||||
let oldList = viewModel.invitationList
|
||||
viewModel.invitationList = invitationList
|
||||
if viewModel.userListType == .notInRoomUsers {
|
||||
self.updateInvitationTableView(oldList: oldList, newList: invitationList)
|
||||
self.updateBottomControlView()
|
||||
}
|
||||
self.updateListStateView()
|
||||
}
|
||||
.store(in: &cancellableSet)
|
||||
}
|
||||
|
||||
func setupViewState() {
|
||||
memberLabel.text = String(format: .memberText, viewModel.allUserCount)
|
||||
let roomInfo = viewModel.roomInfo
|
||||
muteAllAudioButton.isSelected = roomInfo.isMicrophoneDisableForAllUser
|
||||
muteAllVideoButton.isSelected = roomInfo.isCameraDisableForAllUser
|
||||
bottomControlView.isHidden = !viewModel.isShownBottomControlView
|
||||
setupListStateViewText()
|
||||
}
|
||||
|
||||
private func setupListStateViewText() {
|
||||
if viewModel.isSeatEnabled {
|
||||
let seatedListText: String = localizedReplace(.onStageNumberText, replace: String(viewModel.onStageCount))
|
||||
onStageButton.setTitle(seatedListText, for: .normal)
|
||||
let offSeatListText: String = localizedReplace(.notOnStageNumberText, replace: String(viewModel.offStageCount))
|
||||
offStageButton.setTitle(offSeatListText, for: .normal)
|
||||
} else {
|
||||
let haveEnteredListText: String = localizedReplace(.haveEnterenRoomText, replace: String(viewModel.allUserCount))
|
||||
haveEnteredButton.setTitle(haveEnteredListText, for: .normal)
|
||||
}
|
||||
let invitationListText: String = localizedReplace(.notEnteredRoomText, replace: String(viewModel.invitationUserList.count))
|
||||
invatationListButton.setTitle(invitationListText, for: .normal)
|
||||
}
|
||||
|
||||
private func updateBottomControlView() {
|
||||
if viewModel.userListType == .notInRoomUsers {
|
||||
bottomControlView.isHidden = true
|
||||
callEveryoneButton.isHidden = viewModel.invitationList.isEmpty
|
||||
} else {
|
||||
callEveryoneButton.isHidden = true
|
||||
bottomControlView.isHidden = !viewModel.isShownBottomControlView
|
||||
}
|
||||
setupUserListTableView()
|
||||
}
|
||||
|
||||
@objc func muteAllAudioAction(sender: UIButton) {
|
||||
viewModel.muteAllAudioAction(sender: sender, view: self)
|
||||
}
|
||||
|
||||
@objc func muteAllVideoAction(sender: UIButton) {
|
||||
viewModel.muteAllVideoAction(sender: sender, view: self)
|
||||
}
|
||||
|
||||
@objc func moreFunctionAction(sender: UIButton) {
|
||||
RoomRouter.shared.presentPopUpViewController(viewType: .inviteViewType, height: 158.scale375Height())
|
||||
}
|
||||
|
||||
@objc func hideSearchControl(sender: UIView) {
|
||||
if #available(iOS 13, *) {
|
||||
searchBar.searchTextField.resignFirstResponder()
|
||||
} else {
|
||||
searchBar.resignFirstResponder()
|
||||
}
|
||||
searchControl.isHidden = true
|
||||
}
|
||||
|
||||
@objc func selectAllUserAction(sender: UIButton) {
|
||||
guard sender.isSelected != true else { return }
|
||||
sender.isSelected = true
|
||||
invatationListButton.isSelected = false
|
||||
viewModel.changeListState(type: .allUsers)
|
||||
updateBottomControlView()
|
||||
}
|
||||
|
||||
@objc func selectOnStageAction(sender: UIButton) {
|
||||
guard sender.isSelected != true else { return }
|
||||
sender.isSelected = true
|
||||
offStageButton.isSelected = false
|
||||
invatationListButton.isSelected = false
|
||||
viewModel.changeListState(type: .onStageUsers)
|
||||
updateBottomControlView()
|
||||
}
|
||||
|
||||
@objc func selectOffStageAction(sender: UIButton) {
|
||||
guard sender.isSelected != true else { return }
|
||||
sender.isSelected = true
|
||||
onStageButton.isSelected = false
|
||||
invatationListButton.isSelected = false
|
||||
viewModel.changeListState(type: .offStageUsers)
|
||||
updateBottomControlView()
|
||||
}
|
||||
|
||||
@objc func selectInvitationListAction(sender: UIButton) {
|
||||
guard sender.isSelected != true else { return }
|
||||
sender.isSelected = true
|
||||
haveEnteredButton.isSelected = false
|
||||
onStageButton.isSelected = false
|
||||
offStageButton.isSelected = false
|
||||
viewModel.changeListState(type: .notInRoomUsers)
|
||||
updateBottomControlView()
|
||||
}
|
||||
|
||||
@objc func callEveryoneAction(sender: UIButton) {
|
||||
let userIdsNeedtoCall = viewModel.invitationList
|
||||
.filter { $0.status != .pending }
|
||||
.map { $0.invitee.userId }
|
||||
conferenceStore.dispatch(action: ConferenceInvitationActions.inviteUsers(payload: (viewModel.roomInfo.roomId, userIdsNeedtoCall)))
|
||||
}
|
||||
|
||||
func updateInvitationTableView(oldList: [TUIInvitation], newList: [TUIInvitation]) {
|
||||
let result = viewModel.compareLists(oldList: oldList, newList: newList)
|
||||
|
||||
userListTableView.beginUpdates()
|
||||
for invitation in result.removed {
|
||||
if let index = oldList.firstIndex(where: { $0.invitee.userId == invitation.invitee.userId }) {
|
||||
userListTableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
|
||||
viewModel.attendeeList.remove(at: index)
|
||||
}
|
||||
}
|
||||
for invitation in result.added {
|
||||
viewModel.attendeeList.insert(UserEntity(invitation: invitation), at: 0)
|
||||
userListTableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
|
||||
}
|
||||
for invitation in result.changed {
|
||||
if let index = oldList.firstIndex(where: { $0.invitee.userId == invitation.invitee.userId }) {
|
||||
if invitation.status == .rejected {
|
||||
DispatchQueue.main.async {
|
||||
if let cell = self.userListTableView.cellForRow(at: IndexPath(row: index, section: 0)) as? UserListCell {
|
||||
cell.showNotJoiningLabel()
|
||||
}
|
||||
}
|
||||
}
|
||||
userListTableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .none)
|
||||
}
|
||||
}
|
||||
userListTableView.endUpdates()
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
|
||||
@Injected(\.conferenceStore) private var conferenceStore
|
||||
}
|
||||
|
||||
extension UserListView: UISearchBarDelegate {
|
||||
func searchBar(_ searchBar:UISearchBar,textDidChange searchText:String){
|
||||
let searchContentText = searchText.trimmingCharacters(in: .whitespaces)
|
||||
viewModel.searchText = searchContentText
|
||||
viewModel.isSearching = searchContentText.count != 0
|
||||
viewModel.updateAttendeeList()
|
||||
userListTableView.reloadData()
|
||||
}
|
||||
|
||||
func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
|
||||
searchControl.isHidden = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension UserListView: UITableViewDataSource {
|
||||
internal func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return viewModel.attendeeList.count
|
||||
}
|
||||
}
|
||||
|
||||
extension UserListView: UITableViewDelegate {
|
||||
internal func tableView(_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let attendeeModel = viewModel.attendeeList[indexPath.row]
|
||||
let cell = UserListCell(attendeeModel: attendeeModel, viewModel: viewModel)
|
||||
cell.selectionStyle = .none
|
||||
return cell
|
||||
}
|
||||
|
||||
internal func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let attendeeModel = viewModel.attendeeList[indexPath.row]
|
||||
viewModel.showUserManageViewAction(userId: attendeeModel.userId, userName: attendeeModel.userName)
|
||||
}
|
||||
|
||||
internal func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return 60.scale375Height()
|
||||
}
|
||||
}
|
||||
|
||||
extension UserListView: UserListViewResponder {
|
||||
func updateUserListTableView() {
|
||||
setupUserListTableView()
|
||||
}
|
||||
|
||||
func updateMemberLabel(count: Int) {
|
||||
memberLabel.text = String(format: .memberText, viewModel.allUserCount)
|
||||
}
|
||||
|
||||
func updateListStateView() {
|
||||
setupListStateViewText()
|
||||
}
|
||||
|
||||
func updateMuteAllAudioButtonState(isSelect: Bool) {
|
||||
muteAllAudioButton.isSelected = isSelect
|
||||
}
|
||||
|
||||
func updateMuteAllVideoButtonState(isSelect: Bool) {
|
||||
muteAllVideoButton.isSelected = isSelect
|
||||
}
|
||||
|
||||
func updateBottomControlView(isHidden: Bool) {
|
||||
bottomControlView.isHidden = isHidden
|
||||
}
|
||||
|
||||
func updateUserManagerViewDisplayStatus(isHidden: Bool) {
|
||||
let model = UserListManagerViewModel(selectUserId: viewModel.userId)
|
||||
let view = UserListManagerView(viewModel: model)
|
||||
view.show(rootView: self)
|
||||
}
|
||||
|
||||
func makeToast(text: String) {
|
||||
RoomRouter.makeToastInCenter(toast: text, duration: 0.5)
|
||||
}
|
||||
|
||||
func reloadUserListView() {
|
||||
userListTableView.reloadData()
|
||||
}
|
||||
|
||||
func showAlert(title: String?, message: String?, sureTitle: String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?) {
|
||||
RoomRouter.presentAlert(title: title, message: message, sureTitle: sureTitle, declineTitle: declineTitle, sureBlock: sureBlock, declineBlock: declineBlock)
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var allMuteAudioText: String {
|
||||
localized("Mute All")
|
||||
}
|
||||
static var allMuteVideoText: String {
|
||||
localized("Stop all video")
|
||||
}
|
||||
static var allUnMuteAudioText: String {
|
||||
localized("Unmute all")
|
||||
}
|
||||
static var allUnMuteVideoText: String {
|
||||
localized("Enable all video")
|
||||
}
|
||||
static var moreText: String {
|
||||
localized("More")
|
||||
}
|
||||
static var memberText: String {
|
||||
localized("Users(%lld)")
|
||||
}
|
||||
static var searchMemberText: String {
|
||||
localized("Search for participants")
|
||||
}
|
||||
static var onStageNumberText: String {
|
||||
localized("On stage(xx)")
|
||||
}
|
||||
static var notOnStageNumberText: String {
|
||||
localized("Not on stage(xx)")
|
||||
}
|
||||
static var notEnteredRoomText: String {
|
||||
localized("Not Entered(xx)")
|
||||
}
|
||||
static var haveEnterenRoomText: String {
|
||||
localized("Entered(xx)")
|
||||
}
|
||||
static var callEveryoneText: String {
|
||||
localized("Call all")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
//
|
||||
// ScreenCaptureMaskView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2023/7/17.
|
||||
// Turn on masked View for screen sharing
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ScreenCaptureMaskViewFrameType {
|
||||
case fullScreen
|
||||
case small
|
||||
}
|
||||
|
||||
class ScreenCaptureMaskView: UIView {
|
||||
private var dotsTimer: Timer = Timer()
|
||||
weak var responder: TUIVideoSeatViewResponder?
|
||||
let frameType: ScreenCaptureMaskViewFrameType
|
||||
|
||||
let contentView: UIView = {
|
||||
let view = UIView()
|
||||
return view
|
||||
}()
|
||||
|
||||
let sharingScreenView: UIView = {
|
||||
let view = UIView()
|
||||
return view
|
||||
}()
|
||||
|
||||
let sharingScreenImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.image = UIImage(named: "room_sharingScreen", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let sharingScreenLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = .sharingScreenText
|
||||
label.textColor = UIColor(0xB2BBD1)
|
||||
label.font = UIFont(name: "PingFangSC-Regular", size: 16)
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
return label
|
||||
}()
|
||||
|
||||
let stopScreenButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.setTitle(.shareOffText, for: .normal)
|
||||
button.backgroundColor = UIColor(0xCC3D47)
|
||||
button.layer.cornerRadius = 6.scale375()
|
||||
button.titleLabel?.font = UIFont(name: "PingFangSC-Regular", size: 12)
|
||||
return button
|
||||
}()
|
||||
|
||||
init(frameType: ScreenCaptureMaskViewFrameType) {
|
||||
self.frameType = frameType
|
||||
super.init(frame: .zero)
|
||||
updateLabelText()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - view layout
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
backgroundColor = UIColor(0x1B1E26)
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
addSubview(contentView)
|
||||
contentView.addSubview(sharingScreenView)
|
||||
contentView.addSubview(stopScreenButton)
|
||||
sharingScreenView.addSubview(sharingScreenImageView)
|
||||
sharingScreenView.addSubview(sharingScreenLabel)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
contentView.snp.makeConstraints{ make in
|
||||
make.centerX.centerY.equalToSuperview()
|
||||
make.width.equalTo(128.scale375())
|
||||
make.height.equalTo(132.scale375())
|
||||
}
|
||||
sharingScreenView.snp.makeConstraints { make in
|
||||
make.top.left.right.equalToSuperview()
|
||||
make.height.equalTo(74.scale375())
|
||||
}
|
||||
stopScreenButton.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.bottom.equalToSuperview()
|
||||
make.width.equalTo(102.scale375())
|
||||
make.height.equalTo(34.scale375())
|
||||
}
|
||||
sharingScreenImageView.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.top.equalToSuperview()
|
||||
make.height.equalTo(48.scale375())
|
||||
make.width.equalTo(48.scale375())
|
||||
}
|
||||
sharingScreenLabel.snp.makeConstraints { make in
|
||||
make.bottom.left.right.equalToSuperview()
|
||||
make.height.equalTo(22.scale375())
|
||||
}
|
||||
}
|
||||
|
||||
private func bindInteraction() {
|
||||
stopScreenButton.addTarget(self, action: #selector(stopScreenCaptureAction(sender:)), for: .touchUpInside)
|
||||
addGesture()
|
||||
}
|
||||
|
||||
private func addGesture() {
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(clickMask))
|
||||
addGestureRecognizer(tap)
|
||||
}
|
||||
|
||||
@objc func stopScreenCaptureAction(sender: UIButton) {
|
||||
RoomRouter.presentAlert(title: .toastTitleText, message: .toastMessageText, sureTitle: .toastStopText, declineTitle: .toastCancelText, sureBlock: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.responder?.stopScreenCapture()
|
||||
}, declineBlock: nil)
|
||||
}
|
||||
|
||||
@objc func clickMask() {
|
||||
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_ChangeToolBarHiddenState, param: [:])
|
||||
guard RoomRouter.shared.hasChatWindow() else { return }
|
||||
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_HiddenChatWindow, param: [:])
|
||||
}
|
||||
|
||||
func updateLabelText() {
|
||||
var dots = ""
|
||||
dotsTimer = Timer(timeInterval: 1.0, repeats: true) { [weak self] timer in
|
||||
guard let self = self else { return }
|
||||
if dots.count == 3 {
|
||||
dots.removeAll()
|
||||
}
|
||||
dots.append(".")
|
||||
self.sharingScreenLabel.text? = .sharingScreenText + dots
|
||||
}
|
||||
RunLoop.current.add(dotsTimer, forMode: .default)
|
||||
}
|
||||
|
||||
deinit {
|
||||
dotsTimer.invalidate()
|
||||
debugPrint("deinit:\(self)")
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var sharingScreenText: String {
|
||||
localized("You are sharing your screen")
|
||||
}
|
||||
static var shareOffText: String {
|
||||
localized("Stop")
|
||||
}
|
||||
static var toastTitleText: String {
|
||||
localized("Share Screen")
|
||||
}
|
||||
static var toastMessageText: String {
|
||||
localized("Stop TUIRoom screen sharing screen live?")
|
||||
}
|
||||
static var toastCancelText: String {
|
||||
localized("Cancel")
|
||||
}
|
||||
static var toastStopText: String {
|
||||
localized("Stop")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
//
|
||||
// VideoSeatCell.swift
|
||||
// TUIVideoSeat
|
||||
//
|
||||
// Created by WesleyLei on 2021/12/16.
|
||||
// Copyright © 2021 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
class VideoSeatCell: UICollectionViewCell {
|
||||
var seatItem: VideoSeatItem?
|
||||
var isSupportedAmplification: Bool {
|
||||
return seatItem?.videoStreamType == .screenStream
|
||||
}
|
||||
|
||||
private lazy var scrollRenderView: UIScrollView = {
|
||||
let scrollView = UIScrollView()
|
||||
scrollView.backgroundColor = UIColor(0x17181F)
|
||||
scrollView.layer.cornerRadius = 16
|
||||
scrollView.layer.masksToBounds = true
|
||||
scrollView.layer.borderWidth = 2
|
||||
scrollView.layer.borderColor = UIColor.clear.cgColor
|
||||
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
scrollView.showsHorizontalScrollIndicator = false
|
||||
scrollView.maximumZoomScale = 5
|
||||
scrollView.minimumZoomScale = 1
|
||||
scrollView.isScrollEnabled = false
|
||||
scrollView.delegate = self
|
||||
return scrollView
|
||||
}()
|
||||
|
||||
let renderView: UIView = {
|
||||
let view = UIView(frame: .zero)
|
||||
view.backgroundColor = .clear
|
||||
return view
|
||||
}()
|
||||
|
||||
let backgroundMaskView: UIView = {
|
||||
let view = UIView(frame: .zero)
|
||||
view.backgroundColor = UIColor(0x17181F)
|
||||
view.layer.cornerRadius = 16
|
||||
view.layer.masksToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
let userInfoView: VideoSeatUserStatusView = {
|
||||
let view = VideoSeatUserStatusView()
|
||||
return view
|
||||
}()
|
||||
|
||||
let avatarImageView: UIImageView = {
|
||||
let imageView = UIImageView(frame: .zero)
|
||||
imageView.layer.masksToBounds = true
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private var isViewReady = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else {
|
||||
return
|
||||
}
|
||||
isViewReady = true
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
contentView.backgroundColor = .clear
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
scrollRenderView.addSubview(renderView)
|
||||
scrollRenderView.addSubview(backgroundMaskView)
|
||||
contentView.addSubview(scrollRenderView)
|
||||
contentView.addSubview(avatarImageView)
|
||||
contentView.addSubview(userInfoView)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
scrollRenderView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(2)
|
||||
}
|
||||
renderView.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.width.equalToSuperview()
|
||||
make.height.equalToSuperview()
|
||||
}
|
||||
backgroundMaskView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
userInfoView.snp.makeConstraints { make in
|
||||
make.height.equalTo(24)
|
||||
make.bottom.equalToSuperview().offset(-5)
|
||||
make.leading.equalToSuperview().offset(5)
|
||||
make.width.lessThanOrEqualTo(self).multipliedBy(0.9)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func resetVolumeView() {
|
||||
guard let seatItem = seatItem else { return }
|
||||
userInfoView.updateUserVolume(hasAudio: seatItem.hasAudioStream, volume: 0)
|
||||
scrollRenderView.layer.borderColor = UIColor.clear.cgColor
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
scrollRenderView.zoomScale = 1.0
|
||||
}
|
||||
|
||||
deinit {
|
||||
NSObject.cancelPreviousPerformRequests(withTarget: self)
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
|
||||
extension VideoSeatCell: UIScrollViewDelegate {
|
||||
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
||||
return isSupportedAmplification ? renderView : nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
extension VideoSeatCell {
|
||||
func updateUI(item: VideoSeatItem) {
|
||||
seatItem = item
|
||||
let placeholder = UIImage(named: "room_default_user", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
avatarImageView.sd_setImage(with: URL(string: item.avatarUrl), placeholderImage: placeholder)
|
||||
avatarImageView.isHidden = item.videoStreamType == .screenStream ? true : item.hasVideoStream
|
||||
backgroundMaskView.isHidden = item.videoStreamType == .screenStream ? true : item.hasVideoStream
|
||||
userInfoView.updateUserStatus(item)
|
||||
resetVolumeView()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let width = min(self.mm_w / 2, 72)
|
||||
self.avatarImageView.layer.cornerRadius = width * 0.5
|
||||
guard let _ = self.avatarImageView.superview else { return }
|
||||
self.avatarImageView.snp.remakeConstraints { make in
|
||||
make.height.width.equalTo(width)
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateUIVolume(item: VideoSeatItem) {
|
||||
userInfoView.updateUserVolume(hasAudio: item.hasAudioStream, volume: item.userVoiceVolume)
|
||||
if item.userVoiceVolume > 0 && item.hasAudioStream {
|
||||
if item.videoStreamType != .screenStream {
|
||||
scrollRenderView.layer.borderColor = UIColor(0xA5FE33).cgColor
|
||||
}
|
||||
} else {
|
||||
scrollRenderView.layer.borderColor = UIColor.clear.cgColor
|
||||
}
|
||||
resetVolume()
|
||||
}
|
||||
|
||||
func resetVolume() {
|
||||
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(resetVolumeView), object: nil)
|
||||
perform(#selector(resetVolumeView), with: nil, afterDelay: 1)
|
||||
}
|
||||
}
|
||||
|
||||
class TUIVideoSeatDragCell: VideoSeatCell {
|
||||
typealias DragCellClickBlock = () -> Void
|
||||
var clickBlock: DragCellClickBlock?
|
||||
|
||||
private var isViewReady = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else {
|
||||
return
|
||||
}
|
||||
isViewReady = true
|
||||
bindInteraction()
|
||||
}
|
||||
|
||||
private func bindInteraction() {
|
||||
addGesture()
|
||||
}
|
||||
|
||||
func updateSize(size: CGSize) {
|
||||
var frame = self.frame
|
||||
frame.size = size
|
||||
self.frame = frame
|
||||
center = adsorption(centerPoint: center)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - gesture
|
||||
|
||||
extension TUIVideoSeatDragCell {
|
||||
private func addGesture() {
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(click))
|
||||
addGestureRecognizer(tap)
|
||||
let dragGesture = UIPanGestureRecognizer(target: self, action: #selector(dragViewDidDrag(gesture:)))
|
||||
addGestureRecognizer(dragGesture)
|
||||
}
|
||||
|
||||
@objc private func click() {
|
||||
clickBlock?()
|
||||
}
|
||||
|
||||
@objc private func dragViewDidDrag(gesture: UIPanGestureRecognizer) {
|
||||
guard let viewSuperview = superview else { return }
|
||||
let moveState = gesture.state
|
||||
let viewCenter = center
|
||||
switch moveState {
|
||||
case .changed:
|
||||
let point = gesture.translation(in: viewSuperview)
|
||||
center = CGPoint(x: viewCenter.x + point.x, y: viewCenter.y + point.y)
|
||||
break
|
||||
case .ended:
|
||||
let point = gesture.translation(in: viewSuperview)
|
||||
let newPoint = CGPoint(x: viewCenter.x + point.x, y: viewCenter.y + point.y)
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
self.center = self.adsorption(centerPoint: newPoint)
|
||||
}
|
||||
break
|
||||
default: break
|
||||
}
|
||||
gesture.setTranslation(.zero, in: viewSuperview)
|
||||
}
|
||||
|
||||
private func adsorption(centerPoint: CGPoint) -> CGPoint {
|
||||
guard let viewSuperview = superview else { return centerPoint }
|
||||
let limitMargin = 5.0
|
||||
let frame = self.frame
|
||||
let point = CGPoint(x: centerPoint.x - frame.width / 2, y: centerPoint.y - frame.height / 2)
|
||||
var newPoint = point
|
||||
if centerPoint.x < (viewSuperview.frame.width / 2) {
|
||||
newPoint.x = limitMargin
|
||||
} else {
|
||||
newPoint.x = viewSuperview.frame.width - frame.width - limitMargin
|
||||
}
|
||||
if point.y <= limitMargin {
|
||||
newPoint.y = limitMargin
|
||||
} else if (point.y + frame.height) > (viewSuperview.frame.height - limitMargin) {
|
||||
newPoint.y = viewSuperview.frame.height - frame.height - limitMargin
|
||||
}
|
||||
return CGPoint(x: newPoint.x + frame.width / 2, y: newPoint.y + frame.height / 2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
//
|
||||
// VideoSeatLayout.swift
|
||||
// TUIVideoSeat
|
||||
//
|
||||
// Created by janejntang on 2023/3/16.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol VideoSeatLayoutDelegate: AnyObject {
|
||||
func updateNumberOfPages(numberOfPages: NSInteger)
|
||||
}
|
||||
|
||||
class VideoSeatLayout: UICollectionViewFlowLayout {
|
||||
private var prePageCount: NSInteger = 1
|
||||
|
||||
private var collectionViewHeight: CGFloat {
|
||||
return collectionView?.bounds.height ?? UIScreen.main.bounds.height
|
||||
}
|
||||
|
||||
private var collectionViewWidth: CGFloat {
|
||||
return collectionView?.bounds.width ?? kScreenWidth
|
||||
}
|
||||
|
||||
private var isPortrait: Bool {
|
||||
return collectionViewHeight > collectionViewWidth
|
||||
}
|
||||
|
||||
private var kVideoSeatCellNumberOfOneRow: CGFloat {
|
||||
return isPortrait ? 2 : 3
|
||||
}
|
||||
|
||||
private var kMaxShowCellCount: Int {
|
||||
return 6
|
||||
}
|
||||
|
||||
private let itemDiffSpace: CGFloat = 5.0
|
||||
|
||||
private var itemWidthHeight: CGFloat {
|
||||
let minimumDistance = min(collectionViewHeight, collectionViewWidth)
|
||||
let availableSpace = minimumDistance - (kVideoSeatCellNumberOfOneRow + 1) * itemDiffSpace
|
||||
if isPortrait {
|
||||
return availableSpace / kVideoSeatCellNumberOfOneRow
|
||||
} else {
|
||||
return availableSpace / (CGFloat(kMaxShowCellCount) / kVideoSeatCellNumberOfOneRow)
|
||||
}
|
||||
}
|
||||
|
||||
private let viewModel: TUIVideoSeatViewModel
|
||||
|
||||
fileprivate var layoutAttributeArray: [UICollectionViewLayoutAttributes] = []
|
||||
|
||||
init(viewModel: TUIVideoSeatViewModel) {
|
||||
self.viewModel = viewModel
|
||||
super.init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func prepare() {
|
||||
super.prepare()
|
||||
calculateEachCellFrame()
|
||||
}
|
||||
|
||||
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
|
||||
return layoutAttributeArray
|
||||
}
|
||||
|
||||
override var collectionViewContentSize: CGSize {
|
||||
return CGSize(width: CGFloat(prePageCount) * collectionViewWidth, height: collectionViewHeight)
|
||||
}
|
||||
|
||||
weak var delegate: VideoSeatLayoutDelegate?
|
||||
|
||||
func getMiniscreenFrame(item: VideoSeatItem?) -> CGRect {
|
||||
var height = isPortrait ? 180.0 : 100.0
|
||||
var width = isPortrait ? 100.0 : 180.0
|
||||
if let item = item, !item.hasVideoStream {
|
||||
height = 100.0
|
||||
width = 100.0
|
||||
}
|
||||
return CGRect(x: collectionViewWidth - width - itemDiffSpace, y: itemDiffSpace, width: width, height: height)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - layout
|
||||
|
||||
extension VideoSeatLayout {
|
||||
private func calculateEachCellFrame() {
|
||||
guard let collectionViewWidth: CGFloat = collectionView?.bounds.width else { return }
|
||||
guard viewModel.listSeatItem.count > 0 else { return }
|
||||
layoutAttributeArray = []
|
||||
let section: Int = 0
|
||||
prePageCount = 1
|
||||
if viewModel.videoSeatViewType == .singleType {
|
||||
let indexPath = IndexPath(item: 0, section: 0)
|
||||
let cell = getFullScreenAttributes(indexPath: indexPath)
|
||||
layoutAttributeArray.append(cell)
|
||||
} else if viewModel.videoSeatViewType == .largeSmallWindowType {
|
||||
let largeIndexPath = IndexPath(item: 0, section: section)
|
||||
let largeCell = getFullScreenAttributes(indexPath: largeIndexPath)
|
||||
layoutAttributeArray.append(largeCell)
|
||||
let smallIndexPath = IndexPath(item: 1, section: section)
|
||||
let smallCell = getSmallAttributes(indexPath: smallIndexPath)
|
||||
layoutAttributeArray.append(smallCell)
|
||||
} else if viewModel.videoSeatViewType == .pureAudioType || viewModel.videoSeatViewType == .equallyDividedType {
|
||||
guard let itemCount = collectionView?.numberOfItems(inSection: section) else { return }
|
||||
let isMultipage = itemCount >= kMaxShowCellCount
|
||||
for i in 0 ... itemCount - 1 {
|
||||
let indexPath = IndexPath(item: i, section: section)
|
||||
var cell: UICollectionViewLayoutAttributes
|
||||
if isMultipage {
|
||||
cell = getMultipageEquallyDividedAttributes(indexPath: indexPath, item: i, itemCount: itemCount, leftDiff: 0.0)
|
||||
} else {
|
||||
cell = getEquallyDividedAttributes(indexPath: indexPath, item: i, itemCount: itemCount, leftDiff: 0.0)
|
||||
}
|
||||
layoutAttributeArray.append(cell)
|
||||
}
|
||||
prePageCount = Int(ceil(CGFloat(itemCount) / CGFloat(kMaxShowCellCount)))
|
||||
} else if viewModel.videoSeatViewType == .speechType {
|
||||
guard let itemCount = collectionView?.numberOfItems(inSection: section) else { return }
|
||||
let isMultipage = (itemCount - 1) >= kMaxShowCellCount
|
||||
for i in 0 ... itemCount {
|
||||
let indexPath = IndexPath(item: i, section: section)
|
||||
var cell: UICollectionViewLayoutAttributes
|
||||
if i == 0 {
|
||||
cell = getFullScreenAttributes(indexPath: indexPath)
|
||||
} else if isMultipage {
|
||||
cell = getMultipageEquallyDividedAttributes(indexPath: indexPath, item: i - 1,
|
||||
itemCount: itemCount - 1,
|
||||
leftDiff: collectionViewWidth)
|
||||
} else {
|
||||
cell = getEquallyDividedAttributes(indexPath: indexPath, item: i - 1,
|
||||
itemCount: itemCount - 1,
|
||||
leftDiff: collectionViewWidth)
|
||||
}
|
||||
layoutAttributeArray.append(cell)
|
||||
}
|
||||
prePageCount = Int(ceil(CGFloat(itemCount - 1) / CGFloat(kMaxShowCellCount))) + 1
|
||||
}
|
||||
delegate?.updateNumberOfPages(numberOfPages: prePageCount)
|
||||
}
|
||||
|
||||
// Full screen cell layout information
|
||||
private func getFullScreenAttributes(indexPath: IndexPath) ->
|
||||
UICollectionViewLayoutAttributes {
|
||||
let cell = UICollectionViewLayoutAttributes(forCellWith: indexPath)
|
||||
cell.frame = CGRect(x: 0, y: 0, width: collectionViewWidth, height: collectionViewHeight)
|
||||
return cell
|
||||
}
|
||||
|
||||
private func getSmallAttributes(indexPath: IndexPath) -> UICollectionViewLayoutAttributes {
|
||||
let cell = UICollectionViewLayoutAttributes(forCellWith: indexPath)
|
||||
cell.frame = getMiniscreenFrame(item: nil)
|
||||
return cell
|
||||
}
|
||||
|
||||
private func getEquallyDividedAttributes(indexPath: IndexPath, item: Int, itemCount: Int, leftDiff: CGFloat) ->
|
||||
UICollectionViewLayoutAttributes {
|
||||
/*-----------------item&page¤tPageItemCount&cell-----------------**/
|
||||
let item = item + 1
|
||||
let page = Int(ceil(CGFloat(item) / CGFloat(kMaxShowCellCount)))
|
||||
let currentPageItemCount = min(itemCount, page * kMaxShowCellCount) - (page - 1) * kMaxShowCellCount // Number of items on the current page
|
||||
let cell = UICollectionViewLayoutAttributes(forCellWith: indexPath)
|
||||
|
||||
/*-----------------currentPageAllRow&beginCellY&beginCellLeft-----------------**/
|
||||
let currentPageAllRow = Int(ceil(CGFloat(currentPageItemCount) / CGFloat(kVideoSeatCellNumberOfOneRow))) // Calculate the total number of rows on this page
|
||||
let itemAllHeight = (itemWidthHeight + itemDiffSpace) * CGFloat(currentPageAllRow) - itemDiffSpace
|
||||
let itemAllWidth = (itemWidthHeight + itemDiffSpace) * kVideoSeatCellNumberOfOneRow - itemDiffSpace
|
||||
let beginCellY = (collectionViewHeight - itemAllHeight) * 0.5 // Calculate beginCellTop
|
||||
let beginCellX = (collectionViewWidth - itemAllWidth) * 0.5 // Calculate beginCellTop
|
||||
let beginCellLeft = CGFloat(page - 1) * collectionViewWidth // Calculate beginCellLeft
|
||||
|
||||
/*-----------------itemIndex&column&row-----------------**/
|
||||
let itemIndex = item - (page - 1) * kMaxShowCellCount // What is the number on this page?
|
||||
let column = (itemIndex - 1) % Int(kVideoSeatCellNumberOfOneRow) // Which column of cell is on the current page starting from 0?
|
||||
let row = Int(ceil(CGFloat(itemIndex) / CGFloat(kVideoSeatCellNumberOfOneRow))) // What is the row of cell on the current page?
|
||||
let itemY = beginCellY + (itemWidthHeight + itemDiffSpace) * CGFloat(row - 1)
|
||||
var itemX = 0.0
|
||||
if currentPageAllRow == row {
|
||||
// Adjust the center of the last row
|
||||
let lastRowItemCount = currentPageItemCount - (row - 1) * Int(kVideoSeatCellNumberOfOneRow)
|
||||
let lastRowBeginCellLeft = (collectionViewWidth - (itemWidthHeight + itemDiffSpace) * CGFloat(lastRowItemCount) - itemDiffSpace) * 0.5
|
||||
itemX = lastRowBeginCellLeft + beginCellLeft + (itemWidthHeight + itemDiffSpace) * CGFloat(column)
|
||||
} else {
|
||||
itemX = beginCellX + beginCellLeft + (itemWidthHeight + itemDiffSpace) * CGFloat(column)
|
||||
}
|
||||
cell.frame = CGRect(x: leftDiff + itemX, y: itemY, width: itemWidthHeight, height: itemWidthHeight)
|
||||
return cell
|
||||
}
|
||||
|
||||
private func getMultipageEquallyDividedAttributes(indexPath: IndexPath, item: Int, itemCount: Int, leftDiff: CGFloat) ->
|
||||
UICollectionViewLayoutAttributes {
|
||||
let item = item + 1
|
||||
let page = Int(ceil(CGFloat(item) / CGFloat(kMaxShowCellCount)))
|
||||
let cell = UICollectionViewLayoutAttributes(forCellWith: indexPath)
|
||||
let currentPageAllRow = kMaxShowCellCount / Int(kVideoSeatCellNumberOfOneRow)
|
||||
let itemAllHeight = (itemWidthHeight + itemDiffSpace) * CGFloat(currentPageAllRow) - itemDiffSpace
|
||||
let itemAllWidth = (itemWidthHeight + itemDiffSpace) * kVideoSeatCellNumberOfOneRow - itemDiffSpace
|
||||
let beginCellY = (collectionViewHeight - itemAllHeight) * 0.5
|
||||
let beginCellX = (collectionViewWidth - itemAllWidth) * 0.5
|
||||
let beginCellLeft = CGFloat(page - 1) * collectionViewWidth
|
||||
let itemIndex = item - (page - 1) * kMaxShowCellCount
|
||||
let column = (itemIndex - 1) % Int(kVideoSeatCellNumberOfOneRow)
|
||||
let row = Int(ceil(CGFloat(itemIndex) / CGFloat(kVideoSeatCellNumberOfOneRow)))
|
||||
let itemY = beginCellY + (itemWidthHeight + itemDiffSpace) * CGFloat(row - 1)
|
||||
let itemX = beginCellX + beginCellLeft + (itemWidthHeight + itemDiffSpace) * CGFloat(column)
|
||||
cell.frame = CGRect(x: leftDiff + itemX, y: itemY, width: itemWidthHeight, height: itemWidthHeight)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
//
|
||||
// VideoSeatUserStatusView.swift
|
||||
// TUIVideoSeat
|
||||
//
|
||||
// Created by jack on 2023/3/6.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
class VideoSeatUserStatusView: UIView {
|
||||
private var isShownHomeOwnerImageView: Bool = false
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else {
|
||||
return
|
||||
}
|
||||
isViewReady = true
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
backgroundColor = UIColor(0x22262E, alpha: 0.8)
|
||||
layer.cornerRadius = 12
|
||||
layer.masksToBounds = true
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
addSubview(homeOwnerImageView)
|
||||
addSubview(voiceVolumeImageView)
|
||||
addSubview(userNameLabel)
|
||||
}
|
||||
|
||||
private let homeOwnerImageView: UIImageView = {
|
||||
let imageView = UIImageView(image: UIImage(named: "room_homeowner", in: tuiRoomKitBundle(), compatibleWith: nil))
|
||||
imageView.layer.cornerRadius = 12
|
||||
imageView.layer.masksToBounds = true
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private let userNameLabel: UILabel = {
|
||||
let user = UILabel()
|
||||
user.textColor = .white
|
||||
user.backgroundColor = UIColor.clear
|
||||
user.textAlignment = isRTL ? .right : .left
|
||||
user.numberOfLines = 1
|
||||
user.font = UIFont(name: "PingFangSC-Regular", size: 12)
|
||||
return user
|
||||
}()
|
||||
|
||||
private let voiceVolumeImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private func activateConstraints() {
|
||||
updateOwnerImageConstraints()
|
||||
voiceVolumeImageView.snp.remakeConstraints { make in
|
||||
make.leading.equalTo(homeOwnerImageView.snp.trailing).offset(6.scale375())
|
||||
make.width.height.equalTo(14)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
userNameLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(voiceVolumeImageView.snp.trailing).offset(5)
|
||||
make.centerY.equalToSuperview()
|
||||
make.trailing.equalToSuperview().offset(-8)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateOwnerImageConstraints() {
|
||||
guard let _ = homeOwnerImageView.superview else { return }
|
||||
homeOwnerImageView.snp.remakeConstraints { make in
|
||||
make.leading.equalToSuperview()
|
||||
make.width.height.equalTo(isShownHomeOwnerImageView ? 24 : 0)
|
||||
make.top.bottom.equalToSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
extension VideoSeatUserStatusView {
|
||||
func updateUserStatus(_ item: VideoSeatItem) {
|
||||
if !item.userName.isEmpty {
|
||||
userNameLabel.text = item.userName
|
||||
} else {
|
||||
userNameLabel.text = item.userId
|
||||
}
|
||||
if item.userRole == .roomOwner {
|
||||
homeOwnerImageView.image = UIImage(named: "room_homeowner", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
} else if item.userRole == .administrator {
|
||||
homeOwnerImageView.image = UIImage(named: "room_administrator", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
}
|
||||
isShownHomeOwnerImageView = item.userRole != .generalUser
|
||||
homeOwnerImageView.isHidden = !isShownHomeOwnerImageView
|
||||
updateOwnerImageConstraints()
|
||||
updateUserVolume(hasAudio: item.hasAudioStream, volume: item.userVoiceVolume)
|
||||
}
|
||||
|
||||
func updateUserVolume(hasAudio: Bool, volume: Int) {
|
||||
if hasAudio {
|
||||
let volumeImageName = volume <= 0 ? "room_voice_volume1" : "room_voice_volume2"
|
||||
voiceVolumeImageView.image = UIImage(named: volumeImageName, in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
} else {
|
||||
voiceVolumeImageView.image = UIImage(named: "room_mute_audio", in: tuiRoomKitBundle(), compatibleWith: nil)?.checkOverturn()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
//
|
||||
// TUIVideoSeat.swift
|
||||
// TUIVideoSeat
|
||||
//
|
||||
// Created by WesleyLei on 2022/9/13.
|
||||
// Copyright © 2022 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol TUIVideoSeatViewResponder: AnyObject {
|
||||
func switchPosition()
|
||||
func clickVideoSeat()
|
||||
func startPlayVideoStream(item: VideoSeatItem, renderView: UIView?)
|
||||
func stopPlayVideoStream(item: VideoSeatItem)
|
||||
func updateSpeakerPlayVideoState(currentPageIndex: Int)
|
||||
func stopScreenCapture()
|
||||
}
|
||||
|
||||
class TUIVideoSeatView: UIView {
|
||||
private let CellID_Normal = "VideoSeatCell_Normal"
|
||||
private let CellID_Mini = "VideoSeatCell_Mini"
|
||||
private let viewModel: TUIVideoSeatViewModel
|
||||
private var isViewReady: Bool = false
|
||||
weak var responder: TUIVideoSeatViewResponder?
|
||||
|
||||
private var pageControl: UIPageControl = {
|
||||
let control = UIPageControl()
|
||||
control.currentPage = 0
|
||||
control.numberOfPages = 1
|
||||
control.hidesForSinglePage = true
|
||||
control.isUserInteractionEnabled = false
|
||||
return control
|
||||
}()
|
||||
|
||||
init(viewModel: TUIVideoSeatViewModel) {
|
||||
self.viewModel = viewModel
|
||||
super.init(frame: .zero)
|
||||
viewModel.viewResponder = self
|
||||
responder = viewModel
|
||||
isUserInteractionEnabled = true
|
||||
}
|
||||
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
if let item = moveMiniscreen.seatItem,!moveMiniscreen.isHidden {
|
||||
moveMiniscreen.updateSize(size: videoSeatLayout.getMiniscreenFrame(item: item).size)
|
||||
}
|
||||
let offsetYu = Int(attendeeCollectionView.contentOffset.x) % Int(attendeeCollectionView.mm_w)
|
||||
let offsetMuti = CGFloat(offsetYu) / attendeeCollectionView.mm_w
|
||||
let currentPage = (offsetMuti > 0.5 ? 1 : 0) + (Int(attendeeCollectionView.contentOffset.x) / Int(attendeeCollectionView.mm_w))
|
||||
attendeeCollectionView.setContentOffset(
|
||||
CGPoint(x: CGFloat(pageControl.currentPage) * attendeeCollectionView.frame.size.width,
|
||||
y: attendeeCollectionView.contentOffset.y), animated: false)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
lazy var videoSeatLayout: VideoSeatLayout = {
|
||||
let layout = VideoSeatLayout(viewModel: viewModel)
|
||||
layout.delegate = self
|
||||
return layout
|
||||
}()
|
||||
|
||||
lazy var attendeeCollectionView: UICollectionView = {
|
||||
let collection = UICollectionView(frame: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height), collectionViewLayout:
|
||||
videoSeatLayout)
|
||||
collection.register(VideoSeatCell.self, forCellWithReuseIdentifier: CellID_Normal)
|
||||
collection.register(TUIVideoSeatDragCell.self, forCellWithReuseIdentifier: CellID_Mini)
|
||||
collection.isPagingEnabled = true
|
||||
collection.showsVerticalScrollIndicator = false
|
||||
collection.showsHorizontalScrollIndicator = false
|
||||
collection.isUserInteractionEnabled = true
|
||||
collection.contentMode = .scaleToFill
|
||||
collection.backgroundColor = UIColor(0x0F1014)
|
||||
if #available(iOS 11.0, *) {
|
||||
collection.contentInsetAdjustmentBehavior = .never
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
if #available(iOS 10.0, *) {
|
||||
collection.isPrefetchingEnabled = true
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
collection.dataSource = self
|
||||
collection.delegate = self
|
||||
return collection
|
||||
}()
|
||||
|
||||
lazy var moveMiniscreen: TUIVideoSeatDragCell = {
|
||||
let cell = TUIVideoSeatDragCell()
|
||||
cell.frame = videoSeatLayout.getMiniscreenFrame(item: nil)
|
||||
cell.isHidden = true
|
||||
addSubview(cell)
|
||||
return cell
|
||||
}()
|
||||
|
||||
lazy var screenCaptureMaskView: ScreenCaptureMaskView = {
|
||||
let view = ScreenCaptureMaskView(frameType: .fullScreen)
|
||||
view.responder = self.responder
|
||||
view.isHidden = true
|
||||
return view
|
||||
}()
|
||||
|
||||
let placeholderView: UIView = {
|
||||
let view = UIView(frame: .zero)
|
||||
view.isHidden = true
|
||||
return view
|
||||
}()
|
||||
|
||||
func constructViewHierarchy() {
|
||||
backgroundColor = .clear
|
||||
addSubview(placeholderView)
|
||||
addSubview(attendeeCollectionView)
|
||||
addSubview(pageControl)
|
||||
addSubview(screenCaptureMaskView)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
placeholderView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
attendeeCollectionView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
pageControl.snp.makeConstraints { make in
|
||||
make.height.equalTo(24)
|
||||
make.centerX.equalToSuperview()
|
||||
make.bottom.equalToSuperview().offset(-5)
|
||||
}
|
||||
screenCaptureMaskView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
screenCaptureMaskView.isHidden = !EngineManager.shared.store.currentUser.hasScreenStream
|
||||
addGesture()
|
||||
}
|
||||
|
||||
private func addGesture() {
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(clickVideoSeat))
|
||||
addGestureRecognizer(tap)
|
||||
}
|
||||
|
||||
@objc private func clickVideoSeat() {
|
||||
responder?.clickVideoSeat()
|
||||
}
|
||||
|
||||
func updatePageControl() {
|
||||
let offsetYu = Int(attendeeCollectionView.contentOffset.x) % Int(attendeeCollectionView.mm_w)
|
||||
let offsetMuti = CGFloat(offsetYu) / attendeeCollectionView.mm_w
|
||||
pageControl.currentPage = (offsetMuti > 0.5 ? 1 : 0) + (Int(attendeeCollectionView.contentOffset.x) / Int(attendeeCollectionView.mm_w))
|
||||
|
||||
if let seatItem = moveMiniscreen.seatItem, seatItem.hasVideoStream {
|
||||
if pageControl.currentPage == 0 && !moveMiniscreen.isHidden {
|
||||
responder?.startPlayVideoStream(item: seatItem, renderView: moveMiniscreen.renderView)
|
||||
} else {
|
||||
responder?.startPlayVideoStream(item: seatItem, renderView: getVideoVisibleCell(seatItem)?.renderView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TUIVideoSeatViewModelResponder
|
||||
|
||||
extension TUIVideoSeatView: TUIVideoSeatViewModelResponder {
|
||||
private func freshCollectionView(block: () -> Void) {
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
block()
|
||||
CATransaction.commit()
|
||||
}
|
||||
|
||||
func reloadData() {
|
||||
freshCollectionView { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.attendeeCollectionView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
func insertItems(at indexPaths: [IndexPath]) {
|
||||
freshCollectionView { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let cellNumber = self.attendeeCollectionView.numberOfItems(inSection: 0)
|
||||
let listSeatItemNumber = self.viewModel.listSeatItem.count
|
||||
guard cellNumber + indexPaths.count == listSeatItemNumber else { return }
|
||||
self.attendeeCollectionView.performBatchUpdates { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.attendeeCollectionView.insertItems(at: indexPaths)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteItems(at indexPaths: [IndexPath]) {
|
||||
freshCollectionView { [weak self] in
|
||||
guard let self = self else { return }
|
||||
var resultArray: [IndexPath] = []
|
||||
let numberOfSections = self.attendeeCollectionView.numberOfSections
|
||||
for indexPath in indexPaths {
|
||||
let section = indexPath.section
|
||||
let item = indexPath.item
|
||||
guard section < numberOfSections && item < self.attendeeCollectionView.numberOfItems(inSection: section)
|
||||
else { continue }
|
||||
resultArray.append(indexPath)
|
||||
}
|
||||
let cellNumber = self.attendeeCollectionView.numberOfItems(inSection: 0)
|
||||
let listSeatItemNumber = self.viewModel.listSeatItem.count
|
||||
guard cellNumber - indexPaths.count == listSeatItemNumber else { return }
|
||||
self.attendeeCollectionView.performBatchUpdates { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.attendeeCollectionView.deleteItems(at: resultArray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateVideoSeatCellUI(_ item: VideoSeatItem) {
|
||||
if let seatItem = moveMiniscreen.seatItem, seatItem.userId == item.userId {
|
||||
moveMiniscreen.updateUI(item: seatItem)
|
||||
}
|
||||
guard let cell = getVideoVisibleCell(item) else { return }
|
||||
cell.updateUI(item: item)
|
||||
}
|
||||
|
||||
func updateSeatVolume(_ item: VideoSeatItem) {
|
||||
guard let cell = getVideoVisibleCell(item) else { return }
|
||||
cell.updateUIVolume(item: item)
|
||||
}
|
||||
|
||||
func getVideoVisibleCell(_ item: VideoSeatItem) -> VideoSeatCell? {
|
||||
let cellArray = attendeeCollectionView.visibleCells
|
||||
guard let cell = cellArray.first(where: { cell in
|
||||
if let seatCell = cell as? VideoSeatCell, seatCell.seatItem == item {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}) as? VideoSeatCell else { return nil }
|
||||
return cell
|
||||
}
|
||||
|
||||
func updateMiniscreen(_ item: VideoSeatItem?) {
|
||||
guard let item = item else {
|
||||
moveMiniscreen.isHidden = true
|
||||
return
|
||||
}
|
||||
if attendeeCollectionView.contentOffset.x > 0 {
|
||||
return
|
||||
}
|
||||
if let seatItem = moveMiniscreen.seatItem, seatItem.userId != item.userId, (getVideoVisibleCell(seatItem) == nil) {
|
||||
responder?.stopPlayVideoStream(item: seatItem)
|
||||
}
|
||||
moveMiniscreen.updateSize(size: videoSeatLayout.getMiniscreenFrame(item: item).size)
|
||||
moveMiniscreen.isHidden = false
|
||||
bringSubviewToFront(moveMiniscreen)
|
||||
moveMiniscreen.updateUI(item: item)
|
||||
if item.isHasVideoStream {
|
||||
responder?.startPlayVideoStream(item: item, renderView: moveMiniscreen.renderView)
|
||||
}
|
||||
}
|
||||
|
||||
func updateMiniscreenVolume(_ item: VideoSeatItem) {
|
||||
moveMiniscreen.updateUIVolume(item: item)
|
||||
}
|
||||
|
||||
func getMoveMiniscreen() -> TUIVideoSeatDragCell {
|
||||
return moveMiniscreen
|
||||
}
|
||||
|
||||
func showScreenCaptureMaskView(isShow: Bool) {
|
||||
screenCaptureMaskView.isHidden = !isShow
|
||||
if isShow {
|
||||
screenCaptureMaskView.superview?.bringSubviewToFront(screenCaptureMaskView)
|
||||
}
|
||||
}
|
||||
|
||||
func destroyVideoSeatResponder() {
|
||||
responder = nil
|
||||
attendeeCollectionView.delegate = nil
|
||||
attendeeCollectionView.dataSource = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDelegateFlowLayout
|
||||
|
||||
extension TUIVideoSeatView: UICollectionViewDelegateFlowLayout {
|
||||
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
guard let seatItem = viewModel.listSeatItem[safe: indexPath.item] else { return }
|
||||
guard let seatCell = cell as? VideoSeatCell else { return }
|
||||
if seatItem.isHasVideoStream {
|
||||
responder?.startPlayVideoStream(item: seatItem, renderView: seatCell.renderView)
|
||||
} else {
|
||||
responder?.stopPlayVideoStream(item: seatItem)
|
||||
}
|
||||
seatCell.updateUI(item: seatItem)
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
guard let seatCell = cell as? VideoSeatCell else { return }
|
||||
if let seatItem = seatCell.seatItem {
|
||||
responder?.stopPlayVideoStream(item: seatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TUIVideoSeatView: UIScrollViewDelegate {
|
||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
let currentPageIndex = Int(scrollView.contentOffset.x / scrollView.mm_w)
|
||||
responder?.updateSpeakerPlayVideoState(currentPageIndex: currentPageIndex)
|
||||
if currentPageIndex == 0 {
|
||||
addSubview(moveMiniscreen)
|
||||
} else {
|
||||
attendeeCollectionView.addSubview(moveMiniscreen)
|
||||
}
|
||||
updatePageControl()
|
||||
}
|
||||
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
attendeeCollectionView.addSubview(moveMiniscreen)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDataSource
|
||||
|
||||
extension TUIVideoSeatView: UICollectionViewDataSource {
|
||||
func numberOfSections(in collectionView: UICollectionView) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return viewModel.listSeatItem.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
if viewModel.videoSeatViewType == .largeSmallWindowType, indexPath.row == 1 {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CellID_Mini, for: indexPath) as! TUIVideoSeatDragCell
|
||||
cell.clickBlock = {[weak self] in
|
||||
guard let self = self else { return }
|
||||
self.viewModel.switchPosition()
|
||||
}
|
||||
return cell
|
||||
} else {
|
||||
let cell = collectionView.dequeueReusableCell(
|
||||
withReuseIdentifier: CellID_Normal,
|
||||
for: indexPath) as! VideoSeatCell
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDataSource
|
||||
|
||||
extension TUIVideoSeatView: VideoSeatLayoutDelegate {
|
||||
func updateNumberOfPages(numberOfPages: NSInteger) {
|
||||
pageControl.numberOfPages = numberOfPages
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
//
|
||||
// WaterMarkLayer.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/4/3.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class WaterMarkLayer: CALayer {
|
||||
var text: String = ""
|
||||
var textColor: UIColor = UIColor(0x99A2B2).withAlphaComponent(0.3)
|
||||
var lineStyle: WaterMarkLineStyle = .multiLine
|
||||
private var portraitImage: UIImage?
|
||||
private var landscapeImage: UIImage?
|
||||
private let multiLineTextFontSize = 14.0
|
||||
private let sigleLineTextFontSize = 36.0
|
||||
private let singleLineWaterMarkWidth = 303.scale375()
|
||||
private let multiLineWaterMarkWidth = 118.scale375()
|
||||
private let offset = 39.scale375()
|
||||
private var numberOfOneRow: Int {
|
||||
isLandscape ? 4 : 3
|
||||
}
|
||||
private var numberOfOneColumn: Int {
|
||||
isLandscape ? 3 : 4
|
||||
}
|
||||
private var textMinOffset: CGFloat {
|
||||
return lineStyle == .multiLine ? 2 : 4
|
||||
}
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func draw(in ctx: CGContext) {
|
||||
super.draw(in: ctx)
|
||||
let rect:CGRect = .init(origin: .zero, size: CGSize(width: ctx.width, height: ctx.height))
|
||||
ctx.translateBy(x: rect.origin.x, y: rect.origin.y)
|
||||
ctx.translateBy(x: 0, y: rect.size.height)
|
||||
ctx.scaleBy(x: 1.0, y: -1.0)
|
||||
ctx.translateBy(x: -rect.origin.x, y: -rect.origin.y)
|
||||
let waterMarkFullSize = CGSize(width: CGFloat(ctx.width) + offset * 2, height: CGFloat(ctx.height) + offset * 2)
|
||||
guard let image = getWaterMarkImage(isLandScape: isLandscape, andFullSize: waterMarkFullSize)?.cgImage
|
||||
else { return }
|
||||
ctx.draw(image, in: CGRect(origin: CGPoint(x: -offset, y: -offset), size: waterMarkFullSize))
|
||||
}
|
||||
|
||||
private func getWaterMarkImage(isLandScape: Bool, andFullSize fsize: CGSize) -> UIImage? {
|
||||
var image: UIImage?
|
||||
if isLandscape {
|
||||
image = landscapeImage != nil ? landscapeImage : createWatermarkImage(Text: text, andFullSize: fsize)
|
||||
landscapeImage = image
|
||||
} else {
|
||||
image = portraitImage != nil ? portraitImage : createWatermarkImage(Text: text, andFullSize: fsize)
|
||||
portraitImage = image
|
||||
}
|
||||
return image
|
||||
}
|
||||
|
||||
private func createWatermarkImage(Text strTxt:String, andFullSize fsize:CGSize) -> UIImage {
|
||||
let attributedString = getTextAttributeString(text: text)
|
||||
let _size = getTextWaterMarkSize()
|
||||
if UIScreen.main.scale > 1.5 {
|
||||
UIGraphicsBeginImageContextWithOptions(_size,false,0)
|
||||
}
|
||||
else{
|
||||
UIGraphicsBeginImageContext(_size)
|
||||
}
|
||||
//Picture tilt
|
||||
var context = UIGraphicsGetCurrentContext()
|
||||
context?.concatenate(.init(translationX: _size.width * 0.8, y: _size.height * 0.4))
|
||||
context?.concatenate(.init(rotationAngle: -0.25 * .pi))
|
||||
context?.concatenate(.init(translationX: -_size.width * 0.8, y: -_size.height * 0.4))
|
||||
let point = getTextWaterMarkPoint(attributedString: attributedString, size: _size)
|
||||
attributedString.draw(in: .init(origin: point, size: attributedString.size()))
|
||||
let _waterImg = UIGraphicsGetImageFromCurrentImageContext() ?? UIImage.init()
|
||||
if UIScreen.main.scale > 1.5 {
|
||||
UIGraphicsBeginImageContextWithOptions(fsize,false,0)
|
||||
}
|
||||
else{
|
||||
UIGraphicsBeginImageContext(fsize)
|
||||
}
|
||||
context = UIGraphicsGetCurrentContext()
|
||||
if lineStyle == .singleLine {
|
||||
let _rect:CGRect = .init(origin: .init(x: (fsize.width - _size.width) / 2.0,
|
||||
y: (fsize.height - _size.height) / 2.0),
|
||||
size: _waterImg.size)
|
||||
_waterImg.draw(in: _rect)
|
||||
} else {
|
||||
var _tempC = fsize.width / _waterImg.size.width
|
||||
var _maxColumn:Int = _tempC.isNaN || !_tempC.isFinite ? 1 : Int(_tempC)
|
||||
if fsize.width.truncatingRemainder(dividingBy: _waterImg.size.width) != 0 {
|
||||
_maxColumn += 1
|
||||
}
|
||||
_tempC = fsize.height / _waterImg.size.height
|
||||
var _maxRows:Int = _tempC.isNaN || !_tempC.isFinite ? 1 : Int(_tempC)
|
||||
if fsize.height.truncatingRemainder(dividingBy: _waterImg.size.height) != 0 {
|
||||
_maxRows += 1
|
||||
}
|
||||
let spaceX = (fsize.width - multiLineWaterMarkWidth * CGFloat(numberOfOneRow)) / CGFloat(numberOfOneRow - 1)
|
||||
let spaceY = (fsize.height - multiLineWaterMarkWidth * CGFloat(numberOfOneColumn)) / CGFloat(numberOfOneColumn - 1)
|
||||
for r in 0..<_maxRows {
|
||||
for c in 0..<_maxColumn {
|
||||
let _rect:CGRect = .init(origin: .init(x: CGFloat(c) * CGFloat(_waterImg.size.width + spaceX),
|
||||
y: CGFloat(r) * CGFloat(_waterImg.size.height + spaceY)),
|
||||
size: _waterImg.size)
|
||||
_waterImg.draw(in: _rect)
|
||||
}
|
||||
}
|
||||
}
|
||||
context?.clip()
|
||||
context?.setFillColor(UIColor.clear.cgColor)
|
||||
context?.fill(.init(origin: .zero, size: fsize))
|
||||
let _canvasImg = UIGraphicsGetImageFromCurrentImageContext() ?? UIImage.init()
|
||||
UIGraphicsEndImageContext()
|
||||
return _canvasImg
|
||||
}
|
||||
|
||||
private func getTextAttributeString(text: String) -> NSMutableAttributedString {
|
||||
let textFont: CGFloat = lineStyle == .multiLine ? multiLineTextFontSize : sigleLineTextFontSize
|
||||
let paragraphStyle:NSMutableParagraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.lineBreakMode = .byWordWrapping
|
||||
paragraphStyle.lineSpacing = 5
|
||||
paragraphStyle.alignment = .center
|
||||
var textAttributes:[NSAttributedString.Key:Any] = [
|
||||
.font : UIFont.systemFont(ofSize: textFont, weight: .regular),
|
||||
.foregroundColor:textColor,
|
||||
.paragraphStyle: paragraphStyle,
|
||||
.kern:1.0,
|
||||
]
|
||||
if #available(iOS 14.0, *) {
|
||||
textAttributes[.tracking] = 1.0
|
||||
}
|
||||
let attributedString:NSMutableAttributedString = NSMutableAttributedString.init(string: text)
|
||||
let stringRange = NSMakeRange(0, attributedString.string.utf16.count)
|
||||
attributedString.addAttributes(textAttributes,range: stringRange)
|
||||
let viewSize = getTextWaterMarkSize()
|
||||
let maxLength = getViewHypotenuseLength(viewSize: viewSize) - textMinOffset * 2
|
||||
if attributedString.size().width > maxLength, let range = text.range(of: "(") {
|
||||
var wartMarkText = text
|
||||
let location = wartMarkText.distance(from: wartMarkText.startIndex, to: range.lowerBound)
|
||||
let index = wartMarkText.index(wartMarkText.startIndex, offsetBy: location)
|
||||
wartMarkText.insert(contentsOf: "\n", at: index)
|
||||
attributedString.replaceCharacters(in: stringRange, with: wartMarkText)
|
||||
}
|
||||
return attributedString
|
||||
}
|
||||
|
||||
private func getTextWaterMarkSize() -> CGSize {
|
||||
switch lineStyle {
|
||||
case .singleLine:
|
||||
return CGSize(width: singleLineWaterMarkWidth, height: singleLineWaterMarkWidth)
|
||||
case .multiLine:
|
||||
return CGSize(width: multiLineWaterMarkWidth, height: multiLineWaterMarkWidth)
|
||||
}
|
||||
}
|
||||
|
||||
private func getTextWaterMarkPoint(attributedString: NSMutableAttributedString, size: CGSize) -> CGPoint {
|
||||
let viewHypotenuseLength = getViewHypotenuseLength(viewSize: size)
|
||||
var value = (viewHypotenuseLength - attributedString.size().width) / 2.0
|
||||
value = max(value, textMinOffset)
|
||||
return CGPoint(x: value, y: value)
|
||||
}
|
||||
|
||||
private func getViewHypotenuseLength(viewSize: CGSize) -> CGFloat {
|
||||
let square = viewSize.width * viewSize.width + viewSize.height + viewSize.height
|
||||
return sqrt(square)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// WaterMarkLineStyle.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/4/7.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum WaterMarkLineStyle {
|
||||
case singleLine
|
||||
case multiLine
|
||||
}
|
||||
Reference in New Issue
Block a user