// // ContentCreateView.swift // SodaLive // // Created by klaus on 2023/08/11. // import SwiftUI import Kingfisher struct ContentCreateView: View { @StateObject var keyboardHandler = KeyboardHandler() @StateObject private var viewModel = ContentCreateViewModel() @State private var isShowPhotoPicker = false @State private var isShowSelectAudioView = false @State private var isShowSelectThemeView = false var body: some View { BaseView(isLoading: $viewModel.isLoading) { GeometryReader { proxy in ZStack { VStack(spacing: 0) { DetailNavigationBar(title: "콘텐츠 등록") ScrollView(.vertical, showsIndicators: false) { VStack(spacing: 0) { Text("썸네일") .font(.custom(Font.bold.rawValue, size: 16.7)) .foregroundColor(Color(hex: "eeeeee")) .frame(maxWidth: .infinity, alignment: .leading) ZStack { if let selectedImage = viewModel.coverImage { Image(uiImage: selectedImage) .resizable() .scaledToFill() .frame(width: 107, height: 107) .background(Color(hex: "3e3358")) .cornerRadius(8) .clipped() } else { Image("ic_logo") .resizable() .scaledToFit() .padding(13.3) .frame(width: 107, height: 107) .background(Color(hex: "3e3358")) .cornerRadius(8) } Image("ic_camera") .padding(10) .background(Color(hex: "9970ff")) .cornerRadius(30) .offset(x: 50, y: 36) } .frame(alignment: .bottomTrailing) .onTapGesture { isShowPhotoPicker = true } Text("등록") .font(.custom(Font.bold.rawValue, size: 16.7)) .foregroundColor(Color(hex: "eeeeee")) .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 13.3) Text(viewModel.fileName.trimmingCharacters(in: .whitespacesAndNewlines) == "" ? "파일선택" : viewModel.fileName) .font(.custom(Font.medium.rawValue, size: 16.7)) .foregroundColor(Color(hex: "9970ff")) .padding(.vertical, 10) .frame(maxWidth: .infinity) .background(Color(hex: "9970ff").opacity(0.2)) .cornerRadius(5.3) .overlay( RoundedCorner(radius: 8) .stroke(lineWidth: 2) .foregroundColor(Color(hex: "9970ff")) ) .padding(.top, 13.3) .onTapGesture { isShowSelectAudioView = true } } .padding(.top, 13.3) .padding(.horizontal, 13.3) Rectangle() .foregroundColor(Color(hex: "232323")) .frame(height: 6.7) .padding(.top, 26.7) VStack(spacing: 0) { Text("제목") .font(.custom(Font.bold.rawValue, size: 16.7)) .foregroundColor(Color(hex: "eeeeee")) .frame(maxWidth: .infinity, alignment: .leading) TextField("제목을 입력하세요", text: $viewModel.title) .autocapitalization(.none) .disableAutocorrection(true) .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color(hex: "eeeeee")) .padding(.vertical, 16.7) .padding(.horizontal, 13.3) .background(Color(hex: "222222")) .cornerRadius(6.7) .keyboardType(.default) .padding(.top, 13.3) HStack(spacing: 0) { Text("내용") .font(.custom(Font.bold.rawValue, size: 16.7)) .foregroundColor(Color(hex: "eeeeee")) Spacer() Text("\(viewModel.detail.count)자") .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color(hex: "ff5c49")) + Text(" / 최대 500자") .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color(hex: "777777")) } .padding(.top, 26.7) TextViewWrapper( text: $viewModel.detail, placeholder: viewModel.placeholder, textColorHex: "eeeeee", backgroundColorHex: "222222" ) .frame(height: 184) .cornerRadius(6.7) .padding(.top, 13.3) Text("테마") .font(.custom(Font.bold.rawValue, size: 16.7)) .foregroundColor(Color(hex: "eeeeee")) .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 26.7) HStack(spacing: 13.3) { if let theme = viewModel.theme { KFImage(URL(string: theme.image)) .resizable() .frame(width: 33.3, height: 33.3) .clipShape(Circle()) } Text(viewModel.theme != nil ? viewModel.theme!.theme : "테마 선택") .font(.custom(Font.bold.rawValue, size: 16.7)) .foregroundColor(Color(hex: "9970ff")) } .padding(.vertical, viewModel.theme != nil ? 8 : 13.3) .frame(maxWidth: .infinity) .background(Color(hex: "9970ff").opacity(0.2)) .cornerRadius(24) .overlay( RoundedRectangle(cornerRadius: 24) .stroke(lineWidth: 2) .foregroundColor(Color(hex: "9970ff")) ) .padding(.top, 13.3) .onTapGesture { isShowSelectThemeView = true hideKeyboard() } Text("태그") .font(.custom(Font.bold.rawValue, size: 16.7)) .foregroundColor(Color(hex: "eeeeee")) .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 26.7) TextField("예: #연애 #커버곡", text: $viewModel.hashtags) .autocapitalization(.none) .disableAutocorrection(true) .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color(hex: "eeeeee")) .padding(.vertical, 16.7) .padding(.horizontal, 13.3) .background(Color(hex: "222222")) .cornerRadius(6.7) .keyboardType(.default) .padding(.top, 13.3) } .padding(.top, 26.7) .padding(.horizontal, 13.3) Rectangle() .foregroundColor(Color(hex: "232323")) .frame(height: 6.7) .padding(.top, 26.7) VStack(spacing: 13.3) { Text("가격 설정") .font(.custom(Font.bold.rawValue, size: 16.7)) .foregroundColor(Color(hex: "eeeeee")) .frame(maxWidth: .infinity, alignment: .leading) HStack(spacing: 13.3) { SelectButtonView(title: "무료", isChecked: viewModel.isFree) { if !viewModel.isFree { viewModel.isFree = true } } SelectButtonView(title: "유료", isChecked: !viewModel.isFree) { if viewModel.isFree { viewModel.isFree = false } } } if !viewModel.isFree { VStack(spacing: 13.3) { Text("소장 설정") .font(.custom(Font.bold.rawValue, size: 16.7)) .foregroundColor(Color(hex: "eeeeee")) .frame(maxWidth: .infinity, alignment: .leading) HStack(spacing: 13.3) { SelectButtonView(title: "소장/대여", isChecked: !viewModel.isOnlyRental) { if viewModel.isOnlyRental { viewModel.isOnlyRental = false } } SelectButtonView(title: "대여만", isChecked: viewModel.isOnlyRental) { if !viewModel.isOnlyRental { viewModel.isOnlyRental = true } } } } .padding(.top, 13.3) VStack(spacing: 0) { Text(viewModel.isOnlyRental ? "대여 가격" : "소장 가격") .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color(hex: "d2d2d2")) .frame(maxWidth: .infinity, alignment: .leading) HStack(spacing: 0) { TextField("가격을 입력하세요(5캔 이상)", text: $viewModel.priceString) .autocapitalization(.none) .disableAutocorrection(true) .font(.custom(Font.bold.rawValue, size: 14.7)) .foregroundColor(Color(hex: "eeeeee")) .cornerRadius(6.7) .keyboardType(.numberPad) .padding(.trailing, 10) Spacer() Text("캔") .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color(hex: "777777")) } .padding(.vertical, 17) .padding(.horizontal, 13.3) .background(Color(hex: "222222")) .cornerRadius(5.3) .padding(.top, 5.3) Rectangle() .foregroundColor(Color(hex: "232323")) .frame(height: 1) .padding(.top, 11) Text("※ 이용기간 대여 (15일) | 소장 (서비스종료시까지)") .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color(hex: "777777")) .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 13.3) Text("※ 대여가격은 소장가격의 60%로 자동 반영") .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color(hex: "777777")) .frame(maxWidth: .infinity, alignment: .leading) Text("※ 콘텐츠의 최소금액은 5캔 입니다") .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color(hex: "777777")) .frame(maxWidth: .infinity, alignment: .leading) } .padding(.top, 26.7) } } .padding(.top, 26.7) .padding(.horizontal, 13.3) VStack(spacing: 13.3) { Text("연령 제한") .font(.custom(Font.bold.rawValue, size: 16.7)) .foregroundColor(Color(hex: "eeeeee")) .frame(maxWidth: .infinity, alignment: .leading) HStack(spacing: 13.3) { SelectButtonView(title: "전체 연령", isChecked: !viewModel.isAdult) { if viewModel.isAdult { viewModel.isAdult = false } } SelectButtonView(title: "19세 이상", isChecked: viewModel.isAdult) { if !viewModel.isAdult { viewModel.isAdult = true } } } Text("성인콘텐츠를 전체관람가로 등록할 시 발생하는 법적 책임은 회사와 상관없이 콘텐츠를 등록한 본인에게 있습니다.\n콘텐츠 내용은 물론 제목도 19금 여부를 체크해 주시기 바랍니다.") .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color(hex: "DD4500")) .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 13.3) } .padding(.top, 26.7) .padding(.horizontal, 13.3) VStack(spacing: 13.3) { Text("댓글 가능 여부") .font(.custom(Font.bold.rawValue, size: 16.7)) .foregroundColor(Color(hex: "eeeeee")) .frame(maxWidth: .infinity, alignment: .leading) HStack(spacing: 13.3) { SelectButtonView(title: "댓글 가능", isChecked: viewModel.isAvailableComment) { if !viewModel.isAvailableComment { viewModel.isAvailableComment = true } } SelectButtonView(title: "댓글 불가", isChecked: !viewModel.isAvailableComment) { if viewModel.isAvailableComment { viewModel.isAvailableComment = false } } } } .padding(.top, 26.7) .padding(.horizontal, 13.3) if !viewModel.isFree { VStack(spacing: 10) { Text("미리듣기 시간 설정") .font(.custom(Font.bold.rawValue, size: 16.7)) .foregroundColor(Color(hex: "eeeeee")) .frame(maxWidth: .infinity, alignment: .leading) Text("미리듣기 시간을 직접 설정하지 않으면 콘텐츠 앞부분 30초가 자동으로 설정됩니다. 미리듣기의 시간제한은 없습니다.") .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color(hex: "777777")) .frame(maxWidth: .infinity, alignment: .leading) HStack(spacing: 13.3) { VStack(spacing: 5.3) { Text("시작 시간") .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color(hex: "d2d2d2")) .frame(maxWidth: .infinity, alignment: .leading) TextField("00:00:00", text: $viewModel.previewStartTime) .autocapitalization(.none) .disableAutocorrection(true) .font(.custom(Font.bold.rawValue, size: 14.6)) .foregroundColor(Color(hex: "777777")) .padding(.vertical, 16.7) .padding(.horizontal, 13.3) .background(Color(hex: "222222")) .cornerRadius(6.7) .keyboardType(.default) .multilineTextAlignment(.center) } VStack(spacing: 5.3) { Text("종료 시간") .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color(hex: "d2d2d2")) .frame(maxWidth: .infinity, alignment: .leading) TextField("00:00:30", text: $viewModel.previewEndTime) .autocapitalization(.none) .disableAutocorrection(true) .font(.custom(Font.bold.rawValue, size: 14.6)) .foregroundColor(Color(hex: "777777")) .padding(.vertical, 16.7) .padding(.horizontal, 13.3) .background(Color(hex: "222222")) .cornerRadius(6.7) .keyboardType(.default) .multilineTextAlignment(.center) } } .padding(.top, 3.3) } .padding(.top, 26.7) .padding(.horizontal, 13.3) } VStack(spacing: 0) { HStack(alignment: .top, spacing: 0) { Text("등록") .font(.custom(Font.bold.rawValue, size: 18.3)) .foregroundColor(Color.white) .frame(height: 50) .frame(maxWidth: .infinity) .background(Color(hex: "9970ff")) .cornerRadius(10) .padding(13.3) } .frame(maxWidth: .infinity) .background(Color(hex: "222222")) .cornerRadius(16.7, corners: [.topLeft, .topRight]) .onTapGesture { hideKeyboard() viewModel.uploadAudioContent() } Rectangle() .foregroundColor(Color(hex: "222222")) .frame(height: keyboardHandler.keyboardHeight) .frame(maxWidth: .infinity) if proxy.safeAreaInsets.bottom > 0 { Rectangle() .foregroundColor(Color(hex: "222222")) .frame(height: 15.3) .frame(maxWidth: .infinity) } } .padding(.top, 30) } } .fileImporter( isPresented: $isShowSelectAudioView, allowedContentTypes: [.audio], allowsMultipleSelection: false ) { result in switch result { case .success(let url): // Handle selected file URL viewModel.selectedFileUrl = url[0] case .failure(let error): // Handle error if needed print("File import error: \(error.localizedDescription)") } } if isShowPhotoPicker { ImagePicker( isShowing: $isShowPhotoPicker, selectedImage: $viewModel.coverImage, sourceType: .photoLibrary ) } if viewModel.isShowCompletePopup { SodaDialog( title: "콘텐츠 업로드", desc: "등록한 콘텐츠가 업로드 중입니다.\n" + "콘텐츠 등록이 완료되면 알림을 보내드립니다.\n" + "이 페이지를 나가도 콘텐츠는 자동으로 등록됩니다.", confirmButtonTitle: "확인", confirmButtonAction: { AppState.shared.back() }, cancelButtonTitle: "", cancelButtonAction: {} ) } GeometryReader { proxy in VStack { Spacer() ContentCreateSelectThemeView( isShowing: $isShowSelectThemeView, selectedTheme: $viewModel.theme ) .frame(width: proxy.size.width, height: proxy.size.height * 0.9) .offset(y: isShowSelectThemeView ? 0 : proxy.size.height * 0.9) .animation(.easeInOut(duration: 0.49), value: self.isShowSelectThemeView) } } .edgesIgnoringSafeArea(.bottom) } .onTapGesture { hideKeyboard() } .edgesIgnoringSafeArea(.bottom) .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { GeometryReader { geo in HStack { Spacer() Text(viewModel.errorMessage) .padding(.vertical, 13.3) .frame(width: screenSize().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() } } } } } } } struct SelectButtonView: View { let title: String let isChecked: Bool let action: () -> Void var body: some View { HStack(spacing: 6.7) { if isChecked { Image("ic_select_check") } Text(title) .font(.custom(Font.bold.rawValue, size: 14.7)) .foregroundColor(isChecked ? .white : Color(hex: "9970ff")) } .frame(height: 48.7) .frame(maxWidth: .infinity) .background(isChecked ? Color(hex: "9970ff") : Color(hex: "1f1734")) .cornerRadius(6.7) .onTapGesture { hideKeyboard() action() } } } struct ContentCreateView_Previews: PreviewProvider { static var previews: some View { ContentCreateView() } }