// // MainView.swift // SodaLive // import SwiftUI import Bootpay import BootpayUI import Kingfisher struct MainView: View { @StateObject private var viewModel = MainViewModel() @StateObject private var legacyHomeViewModel = HomeViewModel() @StateObject private var liveViewModel = LiveViewModel() @StateObject private var mypageViewModel = MyPageViewModel() @StateObject private var appState = AppState.shared @StateObject private var contentPlayManager = ContentPlayManager.shared @StateObject private var contentPlayerPlayManager = ContentPlayerPlayManager.shared @AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token) @AppStorage("auth") private var auth: Bool = UserDefaults.bool(forKey: UserDefaultsKey.auth) @State private var isShowPlayer = false @State private var isShowAuthView = false @State private var isShowAuthConfirmView = false @State private var pendingAction: (() -> Void)? = nil @State private var isShowLeaveLiveNavigationDialog = false @State private var pendingExternalNavigationAction: (() -> Void)? = nil @State private var pendingExternalNavigationCancelAction: (() -> Void)? = nil @State private var payload = Payload() var body: some View { GeometryReader { proxy in ZStack(alignment: .bottom) { VStack(spacing: 0) { contentView .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(.bottom, appState.isShowPlayer ? 72 : 0) if contentPlayerPlayManager.isShowingMiniPlayer { contentPlayerMiniPlayerView } if contentPlayManager.isShowingMiniPlayer { previewContentMiniPlayerView } } .safeAreaInset(edge: .bottom, spacing: 0) { MainTabBarView( width: proxy.size.width, currentTab: $viewModel.currentTab ) } .onAppear { configurePayload() if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { pushTokenUpdate() legacyHomeViewModel.getMemberInfo() legacyHomeViewModel.getEventPopup() legacyHomeViewModel.addAllPlaybackTracking() } } if appState.isShowNotificationSettingsDialog { NotificationSettingsDialog() } if isShowAuthConfirmView { authConfirmDialog } if liveViewModel.isShowPaymentDialog { LivePaymentDialog( title: liveViewModel.paymentDialogTitle, desc: liveViewModel.paymentDialogDesc, desc2: liveViewModel.paymentDialogDesc2, confirmButtonTitle: liveViewModel.paymentDialogConfirmTitle, confirmButtonAction: liveViewModel.paymentDialogConfirmAction, cancelButtonTitle: liveViewModel.paymentDialogCancelTitle, cancelButtonAction: liveViewModel.hidePopup, startDateTime: liveViewModel.liveStartDate, nowDateTime: liveViewModel.nowDate ) } if liveViewModel.isShowPasswordDialog { LiveRoomPasswordDialog( isShowing: $liveViewModel.isShowPasswordDialog, can: liveViewModel.secretOrPasswordDialogCan, confirmAction: liveViewModel.passwordDialogConfirmAction ) } if let eventItem = appState.eventPopup { VStack(spacing: 0) { Spacer() EventPopupDialogView(eventPopup: eventItem) } .background(Color.black) .onTapGesture { appState.eventPopup = nil } } if isShowPlayer { ContentPlayerView(isShowing: $isShowPlayer, playlist: []) } if appState.isShowPlayer { LiveRoomViewV2() } if isShowLeaveLiveNavigationDialog { leaveLiveNavigationDialog } } .fullScreenCover(isPresented: $isShowAuthView) { authView } .valueChanged(value: appState.pushRoomId) { handlePushRoomId($0) } .valueChanged(value: appState.pushChannelId) { handlePushChannelId($0) } .valueChanged(value: appState.pushMessageId) { handlePushMessageId($0) } .valueChanged(value: appState.pushAudioContentId) { handlePushAudioContentId($0) } .valueChanged(value: appState.pushSeriesId) { handlePushSeriesId($0) } .valueChanged(value: appState.isShowPlayer) { isShowPlayer in guard !isShowPlayer, let pendingExternalNavigationAction = pendingExternalNavigationAction else { return } self.pendingExternalNavigationAction = nil self.pendingExternalNavigationCancelAction = nil DispatchQueue.main.async { pendingExternalNavigationAction() } } .onAppear { if appState.pushMessageId > 0 { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { appState.setAppStep(step: .message) } } } .sodaToast( isPresented: $liveViewModel.isShowPopup, message: liveViewModel.errorMessage, autohideIn: 2 ) } } @ViewBuilder private var contentView: some View { switch viewModel.currentTab { case .home: MainPlaceholderTabView(title: MainTab.home.title) case .content: MainPlaceholderTabView(title: MainTab.content.title) case .chat: MainPlaceholderTabView(title: MainTab.chat.title) case .my: MyPageView() } } private var contentPlayerMiniPlayerView: some View { HStack(spacing: 0) { KFImage(URL(string: contentPlayerPlayManager.coverImageUrl)) .cancelOnDisappear(true) .downsampling(size: CGSize(width: 36.7, height: 36.7)) .resizable() .frame(width: 36.7, height: 36.7) .cornerRadius(5.3) VStack(alignment: .leading, spacing: 2.3) { Text(contentPlayerPlayManager.title) .appFont(size: 13, weight: .medium) .foregroundColor(Color.grayee) .lineLimit(2) Text(contentPlayerPlayManager.nickname) .appFont(size: 11, weight: .medium) .foregroundColor(Color.grayd2) } .padding(.horizontal, 10.7) Spacer() Image(contentPlayerPlayManager.isPlaying ? "ic_noti_pause" : "btn_bar_play") .resizable() .frame(width: 25, height: 25) .onTapGesture { contentPlayerPlayManager.playOrPause() } Image("ic_noti_stop") .resizable() .frame(width: 25, height: 25) .padding(.leading, 16) .onTapGesture { contentPlayerPlayManager.resetPlayer() } } .padding(.vertical, 10.7) .padding(.horizontal, 13.3) .background(Color.gray22) .contentShape(Rectangle()) .onTapGesture { isShowPlayer = true } } private var previewContentMiniPlayerView: some View { HStack(spacing: 0) { KFImage(URL(string: contentPlayManager.coverImage)) .cancelOnDisappear(true) .downsampling(size: CGSize(width: 36.7, height: 36.7)) .resizable() .frame(width: 36.7, height: 36.7) .cornerRadius(5.3) VStack(alignment: .leading, spacing: 2.3) { Text(contentPlayManager.title) .appFont(size: 13, weight: .medium) .foregroundColor(Color.grayee) .lineLimit(2) Text(contentPlayManager.nickname) .appFont(size: 11, weight: .medium) .foregroundColor(Color.grayd2) } .padding(.horizontal, 10.7) Spacer() Image(contentPlayManager.isPlaying ? "ic_noti_pause" : "btn_bar_play") .resizable() .frame(width: 25, height: 25) .onTapGesture { if contentPlayManager.isPlaying { contentPlayManager.pauseAudio() } else { contentPlayManager.playAudio(contentId: contentPlayManager.contentId) } } Image("ic_noti_stop") .resizable() .frame(width: 25, height: 25) .padding(.leading, 16) .onTapGesture { contentPlayManager.stopAudio() } } .padding(.vertical, 10.7) .padding(.horizontal, 13.3) .background(Color.gray22) .contentShape(Rectangle()) .onTapGesture { appState.setAppStep(step: .contentDetail(contentId: contentPlayManager.contentId)) } } private var authConfirmDialog: some View { SodaDialog( title: I18n.Main.Auth.dialogTitle, desc: I18n.Main.Auth.liveEntryVerificationDescription, confirmButtonTitle: I18n.Main.Auth.goToVerification, confirmButtonAction: { isShowAuthConfirmView = false isShowAuthView = true }, cancelButtonTitle: I18n.Common.cancel, cancelButtonAction: { isShowAuthConfirmView = false pendingAction = nil }, textAlignment: .center ) } private var leaveLiveNavigationDialog: some View { SodaDialog( title: I18n.Common.alertTitle, desc: I18n.LiveRoom.leaveLiveForNavigationDesc, confirmButtonTitle: I18n.Common.confirm, confirmButtonAction: { confirmExternalNavigation() }, cancelButtonTitle: I18n.Common.cancel, cancelButtonAction: { cancelExternalNavigation() } ) } private var authView: some View { BootpayUI(payload: payload, requestType: BootpayRequest.TYPE_AUTHENTICATION) .onConfirm { _ in true } .onCancel { _ in isShowAuthView = false } .onError { _ in appState.errorMessage = I18n.Main.Auth.authenticationError appState.isShowErrorPopup = true isShowAuthView = false } .onDone { DEBUG_LOG("onDone: \($0)") mypageViewModel.authVerify($0) { auth = true isShowAuthView = false if let action = pendingAction { pendingAction = nil action() } } } .onClose { isShowAuthView = false } } private func configurePayload() { payload.applicationId = BOOTPAY_APP_ID payload.price = 0 payload.pg = "다날" payload.method = "본인인증" payload.orderName = "본인인증" payload.authenticationId = "\(UserDefaults.string(forKey: .nickname))__\(String(NSTimeIntervalSince1970))" } private func handlePushRoomId(_ value: Int) { guard value > 0 else { return } let roomId = value let isPushRoomFromDeepLink = appState.isPushRoomFromDeepLink appState.pushRoomId = 0 appState.isPushRoomFromDeepLink = false DispatchQueue.main.async { handleExternalNavigationRequest( value: roomId, navigationAction: { if !isPushRoomFromDeepLink { appState.setAppStep(step: .main) } liveViewModel.enterLiveRoom(roomId: roomId) }, cancelAction: { appState.pushRoomId = 0 appState.isPushRoomFromDeepLink = false } ) } } private func handlePushChannelId(_ value: Int) { guard value > 0 else { return } let channelId = value appState.pushChannelId = 0 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { handleExternalNavigationRequest( value: channelId, navigationAction: { appState.setAppStep(step: .main) appState.setAppStep(step: .creatorDetail(userId: channelId)) }, cancelAction: { appState.pushChannelId = 0 } ) } } private func handlePushMessageId(_ value: Int) { guard value > 0 else { return } let messageId = value appState.pushMessageId = 0 DispatchQueue.main.async { handleExternalNavigationRequest( value: messageId, navigationAction: { appState.setAppStep(step: .main) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { appState.setAppStep(step: .message) } }, cancelAction: { appState.pushMessageId = 0 } ) } } private func handlePushAudioContentId(_ value: Int) { guard value > 0 else { return } let contentId = value appState.pushAudioContentId = 0 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { handleExternalNavigationRequest( value: contentId, navigationAction: { appState.setAppStep(step: .main) appState.setAppStep(step: .contentDetail(contentId: contentId)) }, cancelAction: { appState.pushAudioContentId = 0 } ) } } private func handlePushSeriesId(_ value: Int) { guard value > 0 else { return } let seriesId = value appState.pushSeriesId = 0 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { handleExternalNavigationRequest( value: seriesId, navigationAction: { appState.setAppStep(step: .main) appState.setAppStep(step: .seriesDetail(seriesId: seriesId)) }, cancelAction: { appState.pushSeriesId = 0 } ) } } private func handleExternalNavigationRequest( value: Int, navigationAction: @escaping () -> Void, cancelAction: @escaping () -> Void ) { guard value > 0 else { return } if appState.isShowPlayer { pendingExternalNavigationAction = navigationAction pendingExternalNavigationCancelAction = cancelAction isShowLeaveLiveNavigationDialog = true return } navigationAction() } private func confirmExternalNavigation() { guard pendingExternalNavigationAction != nil else { isShowLeaveLiveNavigationDialog = false return } isShowLeaveLiveNavigationDialog = false NotificationCenter.default.post(name: .requestLiveRoomQuitForExternalNavigation, object: nil) } private func cancelExternalNavigation() { isShowLeaveLiveNavigationDialog = false pendingExternalNavigationAction = nil pendingExternalNavigationCancelAction?() pendingExternalNavigationCancelAction = nil } private func pushTokenUpdate() { let pushToken = UserDefaults.string(forKey: .pushToken) if !pushToken.trimmingCharacters(in: .whitespaces).isEmpty { legacyHomeViewModel.pushTokenUpdate(pushToken: pushToken) } } } struct MainView_Previews: PreviewProvider { static var previews: some View { MainView() } }