// // LiveDetailView.swift // SodaLive // // Created by klaus on 2023/08/14. // import SwiftUI import Kingfisher struct LiveDetailView: View { @ObservedObject var viewModel = LiveDetailViewModel() @State private var isExpandParticipantArea = false @State private var isShowCancelPopup = false @StateObject var keyboardHandler = KeyboardHandler() let columns = [ GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()) ] let roomId: Int let onClickParticipant: () -> Void let onClickReservation: () -> Void let onClickStart: () -> Void let onClickCancel: () -> Void var onClickClose: (() -> Void)? = nil var body: some View { BaseView(isLoading: $viewModel.isLoading) { Color.black.opacity(0.7) .onTapGesture { viewModel.onBack { DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { hideView() } } } if isShowCancelPopup { LiveCancelDialog(isShowCancelPopup: $isShowCancelPopup) { reason in viewModel.liveCancel(roomId: roomId, reason: reason) { viewModel.errorMessage = "예약이 취소되었습니다." viewModel.isShowPopup = true onClickCancel() } } } else { GeometryReader { proxy in VStack { Spacer() VStack(spacing: 0) { HStack { Spacer() Image("ic_close_white") .resizable() .frame(width: 20, height: 20) .padding(.top, 13.3) .padding(.trailing, 13.3) .onTapGesture { viewModel.onBack { DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { hideView() } } } } if let room = viewModel.room { ScrollView(.vertical, showsIndicators: false) { VStack(spacing: 0) { HStack(spacing: 5.3) { if room.isAdult { Text("19") .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color(hex: "e33621")) .padding(.horizontal, 5.3) .padding(.vertical, 3.3) .background(Color(hex: "601d14")) .cornerRadius(2.6) } Text(room.title) .font(.custom(Font.bold.rawValue, size: 18.3)) .foregroundColor(Color(hex: "eeeeee")) } .frame(width: proxy.size.width - 26.7, alignment: .leading) .padding(.top, 6.7) HStack(spacing: 0) { Text(room.beginDateTime.convertDateFormat(from: "yyyy.MM.dd EEE hh:mm a", to: "yyyy년 MM월 dd일 (E) a hh시 mm분", locale: Locale(identifier: "en_US_POSIX"))) .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color(hex: "bbbbbb")) Spacer() if room.price > 0 { Text("\(room.price)") .font(.custom(Font.bold.rawValue, size: 18.3)) .foregroundColor(Color(hex: "eeeeee")) Image("ic_can") .resizable() .frame(width: 26.7, height: 26.7) .padding(.leading, 6.7) } else { Text("무료") .font(.custom(Font.bold.rawValue, size: 18.3)) .foregroundColor(Color(hex: "eeeeee")) } } .padding(.top, 16.7) .frame(width: proxy.size.width - 26.7) if UserDefaults.int(forKey: .userId) == room.manager.id { Rectangle() .frame(height: 1) .foregroundColor(Color.gray90.opacity(0.5)) .padding(.top, 8) .frame(width: proxy.size.width - 26.7) ParticipantView(room: room) .frame(width: proxy.size.width - 26.7) } Rectangle() .frame(height: 1) .foregroundColor(Color.gray90.opacity(0.5)) .padding(.top, 13.3) HStack(spacing: 13.3) { let manager = room.manager KFImage(URL(string: manager.profileImageUrl)) .resizable() .scaledToFill() .frame(width: 60, height: 60, alignment: .top) .background(Color(hex: "3e3658")) .clipShape(Circle()) VStack(spacing: 16.7) { HStack(spacing: 6.7) { Text(manager.nickname) .font(.custom(Font.medium.rawValue, size: 16.7)) .foregroundColor(Color(hex: "eeeeee")) Spacer() if let websiteUrl = manager.websiteUrl, let url = URL(string: websiteUrl), UIApplication.shared.canOpenURL(url) { Image("ic_website_purple") .resizable() .frame(width: 33.3, height: 33.3) .onTapGesture { UIApplication.shared.open(url) } } if let blogUrl = manager.blogUrl, let url = URL(string: blogUrl), UIApplication.shared.canOpenURL(url) { Image("ic_blog_purple") .resizable() .frame(width: 33.3, height: 33.3) .onTapGesture { UIApplication.shared.open(url) } } if let instagramUrl = manager.instagramUrl, let url = URL(string: instagramUrl), UIApplication.shared.canOpenURL(url) { Image("ic_instagram_purple") .resizable() .frame(width: 33.3, height: 33.3) .onTapGesture { UIApplication.shared.open(url) } } if let youtubeUrl = manager.youtubeUrl, let url = URL(string: youtubeUrl), UIApplication.shared.canOpenURL(url) { Image("ic_youtube_play_purple") .resizable() .frame(width: 33.3, height: 33.3) .onTapGesture { UIApplication.shared.open(url) } } } HStack(alignment: .center, spacing: 0) { Text(manager.introduce) .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color(hex: "777777")) .lineLimit(3) .fixedSize(horizontal: false, vertical: true) Spacer() if manager.isCreator { HStack(spacing: 3.3) { Image("ic_thumb_play") .resizable() .frame(width: 13.3, height: 13.3) Text("채널보기") .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color.white) .onTapGesture { AppState.shared.setAppStep(step: .creatorDetail(userId: manager.id)) } } .padding(.horizontal, 8.7) .padding(.vertical, 10) .background(Color(hex: "9970ff")) .cornerRadius(16.7) .onTapGesture { } } } } } .padding(.vertical, 20) .padding(.horizontal, 13.3) .frame(width: proxy.size.width) .background(Color(hex: "111111")) Rectangle() .frame(height: 1) .foregroundColor(Color(hex: "909090").opacity(0.5)) Text(room.tags.map { "#\($0)" }.joined(separator: " ")) .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color(hex: "9970ff")) .frame(width: proxy.size.width - 26, alignment: .leading) .padding(.top, 26.7) Text(room.notice) .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color(hex: "777777")) .frame(width: proxy.size.width - 26, alignment: .leading) .padding(.top, 26.7) Rectangle() .frame(width: proxy.size.width - 26.7, height: 1) .foregroundColor(Color(hex: "909090").opacity(0.5)) .padding(.vertical, 40) } } } if !viewModel.isShowPopup { JoinButton() .padding(.bottom, 26.7) } if proxy.safeAreaInsets.bottom > 0 { Rectangle() .foregroundColor(Color(hex: "222222")) .frame(width: screenSize().width, height: 15.3) } } .frame(width: proxy.size.width, height: proxy.size.height * 0.9) .background(Color(hex: "222222")) .cornerRadius(16.7) .offset(y: viewModel.showDetail ? 0 : proxy.size.height * 0.9) .animation(.easeInOut(duration: 0.25), value: viewModel.showDetail) } .edgesIgnoringSafeArea(.bottom) } } if viewModel.isLoading { LoadingView() } } .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 1) { GeometryReader { geo in HStack { Spacer() Text(viewModel.errorMessage) .padding(.vertical, 13.3) .frame(width: geo.size.width - 66.7, alignment: .center) .font(.custom(Font.medium.rawValue, size: 12)) .background(Color(hex: "9970ff")) .foregroundColor(Color.white) .multilineTextAlignment(.center) .cornerRadius(20) .padding(.top, 66.7) Spacer() } .onDisappear { hideView() } } } .sheet( isPresented: $viewModel.isShowShareView, onDismiss: { viewModel.shareMessage = "" }, content: { ActivityViewController(activityItems: [viewModel.shareMessage]) } ) .onAppear { viewModel.getDetail(roomId: roomId) } } @ViewBuilder private func JoinButton() -> some View { if let room = viewModel.room { HStack { if room.channelName.isNullOrBlank() { if room.manager.id == UserDefaults.int(forKey: .userId) { VStack(spacing: 16.7) { HStack(spacing: 13.3) { Text("수정") .font(.custom(Font.bold.rawValue, size: 18.3)) .foregroundColor(Color.white) .padding(.vertical, 16) .padding(.horizontal, 27) .cornerRadius(10) .overlay( RoundedRectangle(cornerRadius: 10) .strokeBorder(lineWidth: 1) .foregroundColor(Color(hex: "9970ff")) ) .onTapGesture { AppState.shared.back() AppState.shared.setAppStep(step: .modifyLive(room: room)) } Text("라이브 시작") .font(.custom(Font.bold.rawValue, size: 18.3)) .foregroundColor(Color.white) .padding(.vertical, 16) .frame(maxWidth: .infinity) .background(Color(hex: "9970ff")) .cornerRadius(10) .onTapGesture { onClickStart() AppState.shared.back() } } Text("예약삭제") .font(.custom(Font.medium.rawValue, size: 14)) .foregroundColor(Color(hex: "ff5c49")) .padding(5.3) .cornerRadius(10) .overlay( RoundedRectangle(cornerRadius: 10) .strokeBorder(lineWidth: 1) .foregroundColor(Color(hex: "dd4500")) ) .onTapGesture { isShowCancelPopup = true } } .frame(width: screenSize().width - 26.7) } else if room.isPaid { Text("예약완료") .font(.custom(Font.bold.rawValue, size: 18.3)) .foregroundColor(Color(hex: "777777")) .padding(.vertical, 16) .padding(.horizontal, 99) .background(Color(hex: "525252")) .cornerRadius(10) .frame(width: screenSize().width - 26.7) } else { Button { onClickReservation() AppState.shared.back() } label: { Text("예약하기") .font(.custom(Font.bold.rawValue, size: 18.3)) .foregroundColor(Color.white) .padding(.vertical, 16) .padding(.horizontal, 99) .background(Color(hex: "9970ff")) .cornerRadius(10) } .frame(width: screenSize().width - 26.7) } } else { Button { onClickParticipant() AppState.shared.back() } label: { Text("지금 참여하기") .font(.custom(Font.bold.rawValue, size: 18.3)) .foregroundColor(Color.white) .padding(.vertical, 16) .padding(.horizontal, 79) .background(Color(hex: "ff5c49")) .cornerRadius(10) } .frame(width: screenSize().width - 26.7) } } } } @ViewBuilder private func ParticipantView(room: GetRoomDetailResponse) -> some View { if isExpandParticipantArea { HStack(spacing: 0) { Text(room.channelName.isNullOrBlank() ? "예약자" : "참가자") .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color.graybb) Spacer() Text("\(room.numberOfParticipants)") .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color.button) Text("/\(room.numberOfParticipantsTotal)") .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color.graybb) } .padding(.top, 16.7) LazyVGrid(columns: columns) { ForEach(room.participatingUsers, id: \.self) { user in VStack(spacing: 6.7) { KFImage(URL(string: user.profileImageUrl)) .resizable() .scaledToFill() .frame(width: 46.7, height: 46.7, alignment: .top) .clipShape(Circle()) Text(user.nickname) .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color.graybb) .lineLimit(1) } } } .padding(.top, 16.7) } else { let userCount = room.numberOfParticipants > 10 ? 10 : room.numberOfParticipants HStack(spacing: -13.3) { ForEach(0.. 0 { HStack(spacing: 6.7) { Image(isExpandParticipantArea ? "ic_live_detail_top" : "ic_live_detail_bottom") .resizable() .frame(width: 20, height: 20) Text(isExpandParticipantArea ? "닫기" : "펼쳐보기") .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color.graybb) } .padding(.top, 13.3) .onTapGesture { isExpandParticipantArea.toggle() } } } private func hideView() { if let close = onClickClose { close() } else { AppState.shared.back() } } }