// // HomeView.swift // SodaLive // // Created by klaus on 2023/08/09. // import SwiftUI import Firebase import Kingfisher import Bootpay import BootpayUI struct HomeView: View { @StateObject var viewModel = HomeViewModel() @StateObject var liveViewModel = LiveViewModel() @StateObject var appState = AppState.shared @StateObject var contentPlayManager = ContentPlayManager.shared @StateObject var contentPlayerPlayManager = ContentPlayerPlayManager.shared @StateObject var mypageViewModel = MyPageViewModel() private var liveView: LiveView { LiveView(onTapLiveNowItem: handleLiveNowItemTap) } private var homeTabView: HomeTabView { HomeTabView( onTapPopularCharacterAllView: { viewModel.currentTab = .chat }, onTapLiveNowItem: handleLiveNowItemTap ) } private let chatTabView = ChatTabView() @State private var isShowPlayer = false @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 isShowAuthView: Bool = false @State private var isShowAuthConfirmView: Bool = false @State private var pendingAction: (() -> Void)? = nil @State private var isShowLeaveLiveNavigationDialog: Bool = 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) { ZStack { homeTabView .frame(width: viewModel.currentTab == .home ? proxy.size.width : 0) .opacity(viewModel.currentTab == .home ? 1.0 : 0.01) liveView .frame(width: viewModel.currentTab == .live ? proxy.size.width : 0) .opacity(viewModel.currentTab == .live ? 1.0 : 0.01) chatTabView .frame(width: viewModel.currentTab == .chat ? proxy.size.width : 0) .opacity(viewModel.currentTab == .chat ? 1.0 : 0.01) if viewModel.currentTab == .mypage { MyPageView() } } .padding(.bottom, appState.isShowPlayer ? 72 : 0) Spacer() if contentPlayerPlayManager.isShowingMiniPlayer { 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 } } if contentPlayManager.isShowingMiniPlayer { 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) ) } } BottomTabView(width: proxy.size.width, currentTab: $viewModel.currentTab) if proxy.safeAreaInsets.bottom > 0 { Rectangle() .foregroundColor(Color.gray11) .frame(width: proxy.size.width, height: 15.3) } } .onAppear { payload.applicationId = BOOTPAY_APP_ID payload.price = 0 payload.pg = "다날" payload.method = "본인인증" payload.orderName = "본인인증" payload.authenticationId = "\(UserDefaults.string(forKey: .nickname))__\(String(NSTimeIntervalSince1970))" if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { pushTokenUpdate() viewModel.getMemberInfo() viewModel.getEventPopup() viewModel.addAllPlaybackTracking() } } if appState.isShowNotificationSettingsDialog { NotificationSettingsDialog() } if isShowAuthConfirmView { SodaDialog( title: "본인인증", desc: "청소년 보호를 위해\n본인인증을 완료한\n성인만 라이브 입장이 가능합니다.\n" + "라이브 입장을 위해\n본인인증을 진행해 주세요.", confirmButtonTitle: "본인인증 하러가기", confirmButtonAction: { isShowAuthConfirmView = false isShowAuthView = true }, cancelButtonTitle: "취소", cancelButtonAction: { isShowAuthConfirmView = false pendingAction = nil }, textAlignment: .center ) } 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) if proxy.safeAreaInsets.bottom > 0 { Rectangle() .foregroundColor(Color(hex: "222222")) .frame(width: proxy.size.width, height: 15.3) } } .background(Color(hex: "222222").opacity(0.7)) .onTapGesture { AppState.shared.eventPopup = nil } } if isShowPlayer { ContentPlayerView(isShowing: $isShowPlayer, playlist: []) } if appState.isShowPlayer { LiveRoomViewV2() } if isShowLeaveLiveNavigationDialog { SodaDialog( title: I18n.Common.alertTitle, desc: I18n.LiveRoom.leaveLiveForNavigationDesc, confirmButtonTitle: I18n.Common.confirm, confirmButtonAction: { confirmExternalNavigation() }, cancelButtonTitle: I18n.Common.cancel, cancelButtonAction: { cancelExternalNavigation() } ) } } .edgesIgnoringSafeArea(.bottom) .fullScreenCover(isPresented: $isShowAuthView) { BootpayUI(payload: payload, requestType: BootpayRequest.TYPE_AUTHENTICATION) .onConfirm { _ in true } .onCancel { _ in isShowAuthView = false } .onError { _ in AppState.shared.errorMessage = "본인인증 중 오류가 발생했습니다." AppState.shared.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 } } .valueChanged(value: appState.pushRoomId) { value in guard value > 0 else { return } let roomId = value appState.pushRoomId = 0 DispatchQueue.main.async { handleExternalNavigationRequest( value: roomId, navigationAction: { appState.setAppStep(step: .main) liveViewModel.enterLiveRoom(roomId: roomId) }, cancelAction: { appState.pushRoomId = 0 } ) } } .valueChanged(value: appState.pushChannelId) { value in 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 } ) } } .valueChanged(value: appState.pushMessageId) { value in 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 } ) } } .valueChanged(value: appState.pushAudioContentId) { value in 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 } ) } } .valueChanged(value: appState.pushSeriesId) { value in 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 } ) } } .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) } } } } } private func handleLiveNowItemTap(roomId: Int, isAdult: Bool) { let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { AppState.shared.setAppStep(step: .login) return } if isAdult && auth == false { pendingAction = { openLiveDetail(roomId: roomId) } isShowAuthConfirmView = true return } openLiveDetail(roomId: roomId) } private func openLiveDetail(roomId: Int) { AppState.shared.setAppStep( step: .liveDetail( roomId: roomId, onClickParticipant: { AppState.shared.isShowPlayer = false liveViewModel.enterLiveRoom(roomId: roomId) }, onClickReservation: {}, onClickStart: {}, onClickCancel: {} ) ) } 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 { self.viewModel.pushTokenUpdate(pushToken: pushToken) } } } extension Notification.Name { static let requestLiveRoomQuitForExternalNavigation = Notification.Name("REQUEST_LIVE_ROOM_QUIT_FOR_EXTERNAL_NAVIGATION") } struct HomeView_Previews: PreviewProvider { static var previews: some View { HomeView() } }