라이브 상세 페이지 추가
This commit is contained in:
parent
e0a5fb733d
commit
634f50d4f2
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "btn_big_share.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/btn_big_share.imageset/btn_big_share.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/btn_big_share.imageset/btn_big_share.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_avatar.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 362 B |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_mic_colored.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_mic_colored.imageset/ic_mic_colored.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/ic_mic_colored.imageset/ic_mic_colored.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -76,4 +76,23 @@ enum AppStep {
|
||||||
timeSettingMode: LiveRoomCreateViewModel.TimeSettingMode,
|
timeSettingMode: LiveRoomCreateViewModel.TimeSettingMode,
|
||||||
onSuccess: (CreateLiveRoomResponse) -> Void
|
onSuccess: (CreateLiveRoomResponse) -> Void
|
||||||
)
|
)
|
||||||
|
|
||||||
|
case liveNowAll(onClickParticipant: (Int) -> Void)
|
||||||
|
|
||||||
|
case liveReservationAll(
|
||||||
|
onClickReservation: (Int) -> Void,
|
||||||
|
onClickStart: (Int) -> Void,
|
||||||
|
onClickCancel: () -> Void,
|
||||||
|
onTapCreateLive: () -> Void
|
||||||
|
)
|
||||||
|
|
||||||
|
case modifyLive(room: GetRoomDetailResponse)
|
||||||
|
|
||||||
|
case liveDetail(
|
||||||
|
roomId: Int,
|
||||||
|
onClickParticipant: () -> Void,
|
||||||
|
onClickReservation: () -> Void,
|
||||||
|
onClickStart: () -> Void,
|
||||||
|
onClickCancel: () -> Void
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,6 +110,29 @@ struct ContentView: View {
|
||||||
onSuccess: onSuccess
|
onSuccess: onSuccess
|
||||||
)
|
)
|
||||||
|
|
||||||
|
case .liveNowAll(let onClickParticipant):
|
||||||
|
LiveNowAllView(onClickParticipant: onClickParticipant)
|
||||||
|
|
||||||
|
case .liveReservationAll(let onClickReservation, let onClickStart, let onClickCancel, let onTapCreateLive):
|
||||||
|
LiveReservationAllView(
|
||||||
|
onClickReservation: onClickReservation,
|
||||||
|
onClickStart: onClickStart,
|
||||||
|
onClickCancel: onClickCancel,
|
||||||
|
onTapCreateLive: onTapCreateLive
|
||||||
|
)
|
||||||
|
|
||||||
|
case .modifyLive(let room):
|
||||||
|
LiveRoomEditView(room: room)
|
||||||
|
|
||||||
|
case .liveDetail(let roomId, let onClickParticipant, let onClickReservation, let onClickStart, let onClickCancel):
|
||||||
|
LiveDetailView(
|
||||||
|
roomId: roomId,
|
||||||
|
onClickParticipant: onClickParticipant,
|
||||||
|
onClickReservation: onClickReservation,
|
||||||
|
onClickStart: onClickStart,
|
||||||
|
onClickCancel: onClickCancel
|
||||||
|
)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
.frame(width: 0, height: 0, alignment: .topLeading)
|
.frame(width: 0, height: 0, alignment: .topLeading)
|
||||||
|
|
|
@ -12,7 +12,7 @@ struct LiveRoomPasswordDialog: View {
|
||||||
@Binding var isShowing: Bool
|
@Binding var isShowing: Bool
|
||||||
|
|
||||||
let can: Int
|
let can: Int
|
||||||
let confirmAction: (Int) -> Void
|
let confirmAction: (String) -> Void
|
||||||
|
|
||||||
@State private var password = ""
|
@State private var password = ""
|
||||||
@StateObject var keyboardHandler = KeyboardHandler()
|
@StateObject var keyboardHandler = KeyboardHandler()
|
||||||
|
@ -81,9 +81,9 @@ struct LiveRoomPasswordDialog: View {
|
||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if password.trimmingCharacters(in: .whitespaces).isEmpty {
|
if password.trimmingCharacters(in: .whitespaces).isEmpty {
|
||||||
confirmAction(0)
|
confirmAction("")
|
||||||
} else {
|
} else {
|
||||||
confirmAction(Int(password)!)
|
confirmAction(password)
|
||||||
}
|
}
|
||||||
isShowing = false
|
isShowing = false
|
||||||
}
|
}
|
||||||
|
@ -97,9 +97,9 @@ struct LiveRoomPasswordDialog: View {
|
||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if password.trimmingCharacters(in: .whitespaces).isEmpty {
|
if password.trimmingCharacters(in: .whitespaces).isEmpty {
|
||||||
confirmAction(0)
|
confirmAction("")
|
||||||
} else {
|
} else {
|
||||||
confirmAction(Int(password)!)
|
confirmAction(password)
|
||||||
}
|
}
|
||||||
isShowing = false
|
isShowing = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,13 +30,9 @@ final class UserProfileViewModel: ObservableObject {
|
||||||
@Published var paymentDialogConfirmAction = {}
|
@Published var paymentDialogConfirmAction = {}
|
||||||
@Published var paymentDialogConfirmTitle = ""
|
@Published var paymentDialogConfirmTitle = ""
|
||||||
|
|
||||||
@Published var secretDialogManagerNickname = ""
|
|
||||||
@Published var secretDialogConfirmAction = {}
|
|
||||||
@Published var isShowSecretDialog = false
|
|
||||||
|
|
||||||
@Published var secretOrPasswordDialogCan = 0
|
@Published var secretOrPasswordDialogCan = 0
|
||||||
|
|
||||||
@Published var passwordDialogConfirmAction: (Int) -> Void = { _ in }
|
@Published var passwordDialogConfirmAction: (String) -> Void = { _ in }
|
||||||
@Published var isShowPasswordDialog = false
|
@Published var isShowPasswordDialog = false
|
||||||
|
|
||||||
@Published var navigationTitle = "채널"
|
@Published var navigationTitle = "채널"
|
||||||
|
@ -101,7 +97,6 @@ final class UserProfileViewModel: ObservableObject {
|
||||||
|
|
||||||
func hidePaymentPopup() {
|
func hidePaymentPopup() {
|
||||||
isShowPaymentDialog = false
|
isShowPaymentDialog = false
|
||||||
isShowSecretDialog = false
|
|
||||||
isShowPasswordDialog = false
|
isShowPasswordDialog = false
|
||||||
|
|
||||||
paymentDialogTitle = ""
|
paymentDialogTitle = ""
|
||||||
|
@ -109,9 +104,6 @@ final class UserProfileViewModel: ObservableObject {
|
||||||
paymentDialogConfirmAction = {}
|
paymentDialogConfirmAction = {}
|
||||||
|
|
||||||
secretOrPasswordDialogCan = 0
|
secretOrPasswordDialogCan = 0
|
||||||
secretDialogManagerNickname = ""
|
|
||||||
secretDialogConfirmAction = {}
|
|
||||||
|
|
||||||
passwordDialogConfirmAction = { _ in }
|
passwordDialogConfirmAction = { _ in }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,7 +136,7 @@ final class UserProfileViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func reservation(roomId: Int, password: Int? = nil) {
|
private func reservation(roomId: Int, password: String? = nil) {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
let request = MakeLiveReservationRequest(roomId: roomId, password: password)
|
let request = MakeLiveReservationRequest(roomId: roomId, password: password)
|
||||||
liveRepository.makeReservation(request: request)
|
liveRepository.makeReservation(request: request)
|
||||||
|
@ -190,14 +182,7 @@ final class UserProfileViewModel: ObservableObject {
|
||||||
self.enterRoom(roomId: roomId)
|
self.enterRoom(roomId: roomId)
|
||||||
}
|
}
|
||||||
} else if ($0.price == 0 || $0.isPaid) {
|
} else if ($0.price == 0 || $0.isPaid) {
|
||||||
if $0.isSecretRoom {
|
if $0.isPrivateRoom {
|
||||||
self.secretDialogManagerNickname = $0.manager.nickname
|
|
||||||
self.secretDialogConfirmAction = {
|
|
||||||
self.enterRoom(roomId: roomId)
|
|
||||||
}
|
|
||||||
self.secretOrPasswordDialogCan = 0
|
|
||||||
self.isShowSecretDialog = true
|
|
||||||
} else if $0.isPrivateRoom {
|
|
||||||
self.passwordDialogConfirmAction = { password in
|
self.passwordDialogConfirmAction = { password in
|
||||||
self.enterRoom(roomId: roomId, password: password)
|
self.enterRoom(roomId: roomId, password: password)
|
||||||
}
|
}
|
||||||
|
@ -208,14 +193,7 @@ final class UserProfileViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if $0.isSecretRoom {
|
if $0.isPrivateRoom {
|
||||||
self.secretDialogManagerNickname = $0.manager.nickname
|
|
||||||
self.secretDialogConfirmAction = {
|
|
||||||
self.enterRoom(roomId: roomId)
|
|
||||||
}
|
|
||||||
self.secretOrPasswordDialogCan = $0.price
|
|
||||||
self.isShowSecretDialog = true
|
|
||||||
} else if $0.isPrivateRoom {
|
|
||||||
self.secretOrPasswordDialogCan = $0.price
|
self.secretOrPasswordDialogCan = $0.price
|
||||||
self.passwordDialogConfirmAction = { password in
|
self.passwordDialogConfirmAction = { password in
|
||||||
self.enterRoom(roomId: roomId, password: password)
|
self.enterRoom(roomId: roomId, password: password)
|
||||||
|
@ -236,7 +214,7 @@ final class UserProfileViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func enterRoom(roomId: Int, onSuccess: (() -> Void)? = nil, password: Int? = nil) {
|
func enterRoom(roomId: Int, onSuccess: (() -> Void)? = nil, password: String? = nil) {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
let request = EnterOrQuitLiveRoomRequest(roomId: roomId, password: password)
|
let request = EnterOrQuitLiveRoomRequest(roomId: roomId, password: password)
|
||||||
liveRepository.enterRoom(request: request)
|
liveRepository.enterRoom(request: request)
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
//
|
||||||
|
// CancelLiveRequest.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct CancelLiveRequest: Encodable {
|
||||||
|
let roomId: Int
|
||||||
|
let reason: String
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
//
|
||||||
|
// LiveCancelDialog.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct LiveCancelDialog: View {
|
||||||
|
|
||||||
|
@Binding var isShowCancelPopup: Bool
|
||||||
|
let confirmAction: (String) -> Void
|
||||||
|
|
||||||
|
@State var reason: String = ""
|
||||||
|
var placeholder = "취소사유를 입력하세요"
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Text("예약취소")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
.padding(.top, 40)
|
||||||
|
|
||||||
|
TextViewWrapper(
|
||||||
|
text: $reason,
|
||||||
|
placeholder: placeholder,
|
||||||
|
textColorHex: "eeeeee",
|
||||||
|
backgroundColorHex: "333333"
|
||||||
|
)
|
||||||
|
.frame(width: screenSize().width - 53.4, height: 150)
|
||||||
|
.cornerRadius(6.7)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 6.7)
|
||||||
|
.stroke(Color(hex: "9970ff"), lineWidth: 1.3)
|
||||||
|
)
|
||||||
|
.padding(.top, 13.3)
|
||||||
|
|
||||||
|
HStack(spacing: 13.3) {
|
||||||
|
Text("취소")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
.padding(.vertical, 16)
|
||||||
|
.padding(.horizontal, 48)
|
||||||
|
.background(Color(hex: "9970ff").opacity(0.2))
|
||||||
|
.cornerRadius(10)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 10)
|
||||||
|
.stroke(Color(hex: "9970ff"), lineWidth: 1.3)
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
isShowCancelPopup = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("확인")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(.vertical, 16)
|
||||||
|
.padding(.horizontal, 48)
|
||||||
|
.background(Color(hex: "9970ff"))
|
||||||
|
.cornerRadius(10)
|
||||||
|
.onTapGesture {
|
||||||
|
confirmAction(reason.trimmingCharacters(in: .whitespacesAndNewlines) != placeholder ? reason : "")
|
||||||
|
isShowCancelPopup = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 45)
|
||||||
|
.padding(.bottom, 16.7)
|
||||||
|
}
|
||||||
|
.frame(width: screenSize().width - 26.7)
|
||||||
|
.background(Color(hex: "222222"))
|
||||||
|
.cornerRadius(10)
|
||||||
|
.onAppear {
|
||||||
|
UITextView.appearance().backgroundColor = .clear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,8 @@ enum LiveApi {
|
||||||
case getRecentRoomInfo
|
case getRecentRoomInfo
|
||||||
case createRoom(parameters: [MultipartFormData])
|
case createRoom(parameters: [MultipartFormData])
|
||||||
case startLive(request: StartLiveRequest)
|
case startLive(request: StartLiveRequest)
|
||||||
|
case cancelRoom(request: CancelLiveRequest)
|
||||||
|
case editLiveRoomInfo(roomId: Int, parameters: [MultipartFormData])
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LiveApi: TargetType {
|
extension LiveApi: TargetType {
|
||||||
|
@ -65,6 +67,12 @@ extension LiveApi: TargetType {
|
||||||
|
|
||||||
case .startLive:
|
case .startLive:
|
||||||
return "/live/room/start"
|
return "/live/room/start"
|
||||||
|
|
||||||
|
case .cancelRoom:
|
||||||
|
return "/live/room/cancel"
|
||||||
|
|
||||||
|
case .editLiveRoomInfo(let roomId, _):
|
||||||
|
return "/live/room/\(roomId)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +84,7 @@ extension LiveApi: TargetType {
|
||||||
case .makeReservation, .enterRoom, .createRoom:
|
case .makeReservation, .enterRoom, .createRoom:
|
||||||
return .post
|
return .post
|
||||||
|
|
||||||
case .cancelReservation, .startLive:
|
case .cancelReservation, .startLive, .cancelRoom, .editLiveRoomInfo:
|
||||||
return .put
|
return .put
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,6 +147,12 @@ extension LiveApi: TargetType {
|
||||||
|
|
||||||
case .startLive(let request):
|
case .startLive(let request):
|
||||||
return .requestJSONEncodable(request)
|
return .requestJSONEncodable(request)
|
||||||
|
|
||||||
|
case .cancelRoom(let request):
|
||||||
|
return .requestJSONEncodable(request)
|
||||||
|
|
||||||
|
case .editLiveRoomInfo(_, let parameters):
|
||||||
|
return .uploadMultipart(parameters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,4 +56,12 @@ final class LiveRepository {
|
||||||
func startLive(roomId: Int) -> AnyPublisher<Response, MoyaError> {
|
func startLive(roomId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
return api.requestPublisher(.startLive(request: StartLiveRequest(roomId: roomId)))
|
return api.requestPublisher(.startLive(request: StartLiveRequest(roomId: roomId)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cancelRoom(roomId: Int, reason: String) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.cancelRoom(request: CancelLiveRequest(roomId: roomId, reason: reason)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func editLiveRoomInfo(roomId: Int, parameters: [MultipartFormData]) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.editLiveRoomInfo(roomId: roomId, parameters: parameters))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,19 +73,22 @@ struct LiveView: View {
|
||||||
height: viewModel.eventBannerItems.count > 0 ? screenSize().width * 300 / 1000 : 0,
|
height: viewModel.eventBannerItems.count > 0 ? screenSize().width * 300 / 1000 : 0,
|
||||||
alignment: .center
|
alignment: .center
|
||||||
)
|
)
|
||||||
.padding(.vertical, 40)
|
.padding(.top, 40)
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewModel.liveReservationItems.count > 0 {
|
if viewModel.liveReservationItems.count > 0 {
|
||||||
SectionLiveReservationView(
|
SectionLiveReservationView(
|
||||||
items: viewModel.liveReservationItems,
|
items: viewModel.liveReservationItems,
|
||||||
onClickCancel: { viewModel.getSummary() },
|
onClickCancel: { viewModel.getSummary() },
|
||||||
onClickStart: {_ in},
|
onClickStart: { roomId in processStart(roomId: roomId) },
|
||||||
onClickReservation: {_ in},
|
onClickReservation: { roomId in
|
||||||
|
viewModel.reservationLiveRoom(roomId: roomId)
|
||||||
|
},
|
||||||
onTapCreateLive: {
|
onTapCreateLive: {
|
||||||
AppState.shared.setAppStep(step: .createLive(timeSettingMode: .RESERVATION, onSuccess: onCreateSuccess))
|
AppState.shared.setAppStep(step: .createLive(timeSettingMode: .RESERVATION, onSuccess: onCreateSuccess))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
.padding(.top, 40)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ final class LiveViewModel: ObservableObject {
|
||||||
@Published var paymentDialogConfirmTitle = ""
|
@Published var paymentDialogConfirmTitle = ""
|
||||||
|
|
||||||
@Published var secretOrPasswordDialogCoin = 0
|
@Published var secretOrPasswordDialogCoin = 0
|
||||||
@Published var passwordDialogConfirmAction: (Int) -> Void = { _ in }
|
@Published var passwordDialogConfirmAction: (String) -> Void = { _ in }
|
||||||
@Published var isShowPasswordDialog = false
|
@Published var isShowPasswordDialog = false
|
||||||
|
|
||||||
@Published var isFollowingList = UserDefaults.bool(forKey: .isFollowedChannel) {
|
@Published var isFollowingList = UserDefaults.bool(forKey: .isFollowedChannel) {
|
||||||
|
@ -52,6 +52,17 @@ final class LiveViewModel: ObservableObject {
|
||||||
var isLast = false
|
var isLast = false
|
||||||
private let pageSize = 10
|
private let pageSize = 10
|
||||||
|
|
||||||
|
var selectedDateString: String = "" {
|
||||||
|
didSet {
|
||||||
|
if !selectedDateString.trimmingCharacters(in: .whitespaces).isEmpty {
|
||||||
|
page = 1
|
||||||
|
isLast = false
|
||||||
|
liveReservationItems.removeAll()
|
||||||
|
getLiveReservationList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func hidePopup() {
|
func hidePopup() {
|
||||||
isShowPaymentDialog = false
|
isShowPaymentDialog = false
|
||||||
isShowPasswordDialog = false
|
isShowPasswordDialog = false
|
||||||
|
@ -176,7 +187,7 @@ final class LiveViewModel: ObservableObject {
|
||||||
.store(in: &subscription)
|
.store(in: &subscription)
|
||||||
}
|
}
|
||||||
|
|
||||||
func enterRoom(roomId: Int, onSuccess: (() -> Void)? = nil, password: Int? = nil) {
|
func enterRoom(roomId: Int, onSuccess: (() -> Void)? = nil, password: String? = nil) {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
let request = EnterOrQuitLiveRoomRequest(roomId: roomId, password: password)
|
let request = EnterOrQuitLiveRoomRequest(roomId: roomId, password: password)
|
||||||
repository.enterRoom(request: request)
|
repository.enterRoom(request: request)
|
||||||
|
@ -260,6 +271,216 @@ final class LiveViewModel: ObservableObject {
|
||||||
.store(in: &subscription)
|
.store(in: &subscription)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getLiveNowList() {
|
||||||
|
if (!isLast && !isLoading) {
|
||||||
|
isLoading = true
|
||||||
|
repository.roomList(
|
||||||
|
request: GetRoomListRequest(
|
||||||
|
timezone: TimeZone.current.identifier,
|
||||||
|
dateString: nil,
|
||||||
|
status: .NOW,
|
||||||
|
page: page,
|
||||||
|
size: pageSize
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.sink { result in
|
||||||
|
switch result {
|
||||||
|
case .finished:
|
||||||
|
DEBUG_LOG("finish")
|
||||||
|
case .failure(let error):
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
}
|
||||||
|
} receiveValue: { [unowned self] response in
|
||||||
|
self.isLoading = false
|
||||||
|
let responseData = response.data
|
||||||
|
|
||||||
|
do {
|
||||||
|
let jsonDecoder = JSONDecoder()
|
||||||
|
let decoded = try jsonDecoder.decode(ApiResponse<[GetRoomListResponse]>.self, from: responseData)
|
||||||
|
|
||||||
|
if let data = decoded.data, decoded.success {
|
||||||
|
if !data.isEmpty {
|
||||||
|
page += 1
|
||||||
|
self.liveNowItems.append(contentsOf: data)
|
||||||
|
} else {
|
||||||
|
isLast = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let message = decoded.message {
|
||||||
|
self.errorMessage = message
|
||||||
|
} else {
|
||||||
|
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLiveReservationList() {
|
||||||
|
if (!isLast && !isLoading) {
|
||||||
|
isLoading = true
|
||||||
|
repository.roomList(
|
||||||
|
request: GetRoomListRequest(
|
||||||
|
timezone: TimeZone.current.identifier,
|
||||||
|
dateString: selectedDateString,
|
||||||
|
status: .RESERVATION,
|
||||||
|
page: page,
|
||||||
|
size: pageSize
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.sink { result in
|
||||||
|
switch result {
|
||||||
|
case .finished:
|
||||||
|
DEBUG_LOG("finish")
|
||||||
|
case .failure(let error):
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
}
|
||||||
|
} receiveValue: { [unowned self] response in
|
||||||
|
self.isLoading = false
|
||||||
|
let responseData = response.data
|
||||||
|
|
||||||
|
do {
|
||||||
|
let jsonDecoder = JSONDecoder()
|
||||||
|
let decoded = try jsonDecoder.decode(ApiResponse<[GetRoomListResponse]>.self, from: responseData)
|
||||||
|
|
||||||
|
if let data = decoded.data, decoded.success {
|
||||||
|
if !data.isEmpty {
|
||||||
|
page += 1
|
||||||
|
self.liveReservationItems.append(contentsOf: data)
|
||||||
|
} else {
|
||||||
|
isLast = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let message = decoded.message {
|
||||||
|
self.errorMessage = message
|
||||||
|
} else {
|
||||||
|
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func reservationLiveRoom(roomId: Int) {
|
||||||
|
getRoomDetail(roomId: roomId) { [unowned self] in
|
||||||
|
if ($0.manager.id == UserDefaults.int(forKey: .userId)) {
|
||||||
|
self.errorMessage = "내가 만든 라이브는 예약할 수 없습니다."
|
||||||
|
self.isShowPopup = true
|
||||||
|
} else {
|
||||||
|
if $0.isPrivateRoom {
|
||||||
|
self.passwordDialogConfirmAction = { password in
|
||||||
|
self.reservation(roomId: roomId, password: password)
|
||||||
|
}
|
||||||
|
self.isShowPasswordDialog = true
|
||||||
|
} else {
|
||||||
|
if ($0.price == 0 || $0.isPaid) {
|
||||||
|
self.reservation(roomId: roomId)
|
||||||
|
} else {
|
||||||
|
self.paymentDialogTitle = "\($0.price)코인으로 예약"
|
||||||
|
self.paymentDialogDesc = "'\($0.title)' 라이브에 참여하기 위해 결제합니다."
|
||||||
|
self.paymentDialogConfirmTitle = "결제 후 예약하기"
|
||||||
|
self.paymentDialogConfirmAction = { [unowned self] in
|
||||||
|
hidePopup()
|
||||||
|
reservation(roomId: roomId)
|
||||||
|
}
|
||||||
|
self.isShowPaymentDialog = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getRoomDetail(roomId: Int, onSuccess: @escaping (GetRoomDetailResponse) -> Void) {
|
||||||
|
isLoading = true
|
||||||
|
repository.getRoomDetail(roomId: roomId)
|
||||||
|
.sink { result in
|
||||||
|
switch result {
|
||||||
|
case .finished:
|
||||||
|
DEBUG_LOG("finish")
|
||||||
|
case .failure(let error):
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
}
|
||||||
|
} receiveValue: { response in
|
||||||
|
let responseData = response.data
|
||||||
|
|
||||||
|
do {
|
||||||
|
let jsonDecoder = JSONDecoder()
|
||||||
|
let decoded = try jsonDecoder.decode(ApiResponse<GetRoomDetailResponse>.self, from: responseData)
|
||||||
|
|
||||||
|
if let data = decoded.data, decoded.success {
|
||||||
|
onSuccess(data)
|
||||||
|
} else {
|
||||||
|
if let message = decoded.message {
|
||||||
|
self.errorMessage = message
|
||||||
|
} else {
|
||||||
|
self.errorMessage = "라이브 정보를 가져오지 못했습니다.\n다시 시도해 주세요."
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
self.errorMessage = "라이브 정보를 가져오지 못했습니다.\n다시 시도해 주세요."
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isLoading = false
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reservation(roomId: Int, password: String? = nil) {
|
||||||
|
isLoading = true
|
||||||
|
let request = MakeLiveReservationRequest(roomId: roomId, password: password)
|
||||||
|
repository.makeReservation(request: request)
|
||||||
|
.sink { result in
|
||||||
|
switch result {
|
||||||
|
case .finished:
|
||||||
|
DEBUG_LOG("finish")
|
||||||
|
case .failure(let error):
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
}
|
||||||
|
} receiveValue: { [unowned self] response in
|
||||||
|
self.isLoading = false
|
||||||
|
let responseData = response.data
|
||||||
|
|
||||||
|
do {
|
||||||
|
let jsonDecoder = JSONDecoder()
|
||||||
|
let decoded = try jsonDecoder.decode(ApiResponse<MakeLiveReservationResponse>.self, from: responseData)
|
||||||
|
|
||||||
|
if let response = decoded.data, decoded.success {
|
||||||
|
self.getSummary()
|
||||||
|
AppState.shared.setAppStep(step: .liveReservationComplete(response: response))
|
||||||
|
} else {
|
||||||
|
if let message = decoded.message {
|
||||||
|
self.errorMessage = message
|
||||||
|
} else {
|
||||||
|
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
}
|
||||||
|
|
||||||
private func getFollowedChannelList() {
|
private func getFollowedChannelList() {
|
||||||
followedChannelItems.removeAll()
|
followedChannelItems.removeAll()
|
||||||
isFollowedChannelLoading = true
|
isFollowedChannelLoading = true
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
//
|
||||||
|
// LiveAllViewModel.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class LiveAllViewModel: ObservableObject {
|
||||||
|
@Published var isShowLiveDetail = false
|
||||||
|
@Published var selectedRoomId = 0
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
//
|
||||||
|
// LiveNowAllItemView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
struct LiveNowAllItemView: View {
|
||||||
|
|
||||||
|
let item: GetRoomListResponse
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 13.3) {
|
||||||
|
HStack(spacing: 20) {
|
||||||
|
ZStack(alignment: .topLeading) {
|
||||||
|
KFImage(URL(string: item.coverImageUrl))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 80, height: 116.7, alignment: .top)
|
||||||
|
.cornerRadius(4.7)
|
||||||
|
.clipped()
|
||||||
|
|
||||||
|
if item.isAdult {
|
||||||
|
Text("19")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 11.3))
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
.padding(4)
|
||||||
|
.background(Color(hex: "e53621"))
|
||||||
|
.cornerRadius(20)
|
||||||
|
.padding(.top, 3.3)
|
||||||
|
.padding(.leading, 3.3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
HStack(alignment: .top, spacing: 0) {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
Text(item.managerNickname)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 11.3))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
|
||||||
|
Text(item.title)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 15.3))
|
||||||
|
.foregroundColor(Color(hex: "e2e2e2"))
|
||||||
|
.lineLimit(2)
|
||||||
|
.padding(.top, 4.3)
|
||||||
|
.padding(.trailing, 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if item.isPrivateRoom {
|
||||||
|
Image("ic_lock")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 13.3)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Image("ic_avatar")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
|
||||||
|
Text("\(item.numberOfParticipate)")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(.leading, 2.7)
|
||||||
|
|
||||||
|
Text("/\(item.numberOfPeople)")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "555555"))
|
||||||
|
|
||||||
|
Text(item.numberOfPeople > item.numberOfParticipate ? "참여가능" : "Sold out")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(
|
||||||
|
Color(
|
||||||
|
hex: item.numberOfPeople > item.numberOfParticipate ?
|
||||||
|
"9970ff" :
|
||||||
|
"ffd300"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(.leading, 10)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if item.price > 0 {
|
||||||
|
Text("\(item.price)")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 15.3))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
Image("ic_can")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
.padding(.leading, 6.7)
|
||||||
|
} else {
|
||||||
|
Text("무료")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 15.3))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.bottom, 3.3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle()
|
||||||
|
.foregroundColor(Color(hex: "909090").opacity(0.5))
|
||||||
|
.frame(width: screenSize().width - 26.7, height: 1)
|
||||||
|
}
|
||||||
|
.frame(width: screenSize().width - 26.7, height: 130, alignment: .center)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
//
|
||||||
|
// LiveNowAllView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import RefreshableScrollView
|
||||||
|
|
||||||
|
struct LiveNowAllView: View {
|
||||||
|
|
||||||
|
@StateObject var viewModel = LiveViewModel()
|
||||||
|
@StateObject var liveAllViewModel = LiveAllViewModel()
|
||||||
|
|
||||||
|
let onClickParticipant: (Int) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
|
GeometryReader { proxy in
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
DetailNavigationBar(title: "지금 라이브 중 전체보기")
|
||||||
|
|
||||||
|
RefreshableScrollView(
|
||||||
|
refreshing: $viewModel.isRefresh,
|
||||||
|
action: {
|
||||||
|
viewModel.getLiveNowList()
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
ForEach(0..<viewModel.liveNowItems.count, id: \.self) { index in
|
||||||
|
let item = viewModel.liveNowItems[index]
|
||||||
|
|
||||||
|
LiveNowAllItemView(item: item)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
self.liveAllViewModel.selectedRoomId = item.roomId
|
||||||
|
withAnimation {
|
||||||
|
self.liveAllViewModel.isShowLiveDetail = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if index == viewModel.liveNowItems.count - 1 {
|
||||||
|
viewModel.getLiveNowList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if proxy.safeAreaInsets.bottom > 0 {
|
||||||
|
Rectangle()
|
||||||
|
.foregroundColor(Color.black.opacity(0))
|
||||||
|
.frame(width: screenSize().width, height: 15.3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
if liveAllViewModel.isShowLiveDetail {
|
||||||
|
LiveDetailView(
|
||||||
|
roomId: liveAllViewModel.selectedRoomId,
|
||||||
|
onClickParticipant: {
|
||||||
|
AppState.shared.isShowPlayer = false
|
||||||
|
onClickParticipant(liveAllViewModel.selectedRoomId)
|
||||||
|
},
|
||||||
|
onClickReservation: {},
|
||||||
|
onClickStart: {},
|
||||||
|
onClickCancel: {},
|
||||||
|
onClickClose: {
|
||||||
|
withAnimation {
|
||||||
|
liveAllViewModel.isShowLiveDetail = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
viewModel.getLiveNowList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
//
|
||||||
|
// LiveNowItemView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
struct LiveNowItemView: View {
|
||||||
|
|
||||||
|
let item: GetRoomListResponse
|
||||||
|
|
||||||
|
let width: CGFloat = 133.3
|
||||||
|
let height: CGFloat = 176.7
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
KFImage(URL(string: item.coverImageUrl))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: width, height: height, alignment: .top)
|
||||||
|
.cornerRadius(4.7)
|
||||||
|
.clipped()
|
||||||
|
|
||||||
|
LinearGradient(
|
||||||
|
colors: [Color.black.opacity(0.1), Color.black.opacity(0.8)],
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom
|
||||||
|
)
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
HStack(spacing: 3.3) {
|
||||||
|
Text(item.price > 0 ? "유료" : "무료")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
.padding(.horizontal, 7.3)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(Color(hex: item.price > 0 ? "881609" : "643bc8"))
|
||||||
|
.cornerRadius(10)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if item.isPrivateRoom {
|
||||||
|
Image("ic_lock")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.isAdult {
|
||||||
|
Text("19")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 11.3))
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
.padding(4)
|
||||||
|
.background(Color(hex: "e53621"))
|
||||||
|
.clipShape(Circle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 3.3)
|
||||||
|
.padding(.top, 3.3)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Image("ic_avatar")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
|
||||||
|
Text("\(item.numberOfParticipate)")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
.padding(.leading, 2.7)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text("\(item.managerNickname)")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 6.7)
|
||||||
|
.padding(.bottom, 6.7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: width, height: height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LiveNowItemView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
LiveNowItemView(
|
||||||
|
item: GetRoomListResponse(
|
||||||
|
roomId: 99,
|
||||||
|
title: "test",
|
||||||
|
content: "testtest",
|
||||||
|
beginDateTime: "2022.05.23 Mon 03:00 PM",
|
||||||
|
numberOfParticipate: 3,
|
||||||
|
numberOfPeople: 5,
|
||||||
|
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||||
|
isAdult: true,
|
||||||
|
price: 0,
|
||||||
|
tags: ["팬미팅", "힐링"],
|
||||||
|
channelName: nil,
|
||||||
|
managerNickname: "user8",
|
||||||
|
managerId: 19,
|
||||||
|
isReservation: false,
|
||||||
|
isPrivateRoom: true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,88 @@ struct SectionLiveNowView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 13.3) {
|
VStack(spacing: 13.3) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("지금 ")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
Text("라이브중")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||||
|
.foregroundColor(Color(hex: "ff5c49"))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if items.count > 0 {
|
||||||
|
Text("전체보기")
|
||||||
|
.font(.custom(Font.light.rawValue, size: 11.3))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
.onTapGesture { AppState.shared.setAppStep(step: .liveNowAll(onClickParticipant: onClickParticipant)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
.frame(width: screenSize().width)
|
||||||
|
|
||||||
|
if items.count > 0 {
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
ForEach(items, id: \.self) { item in
|
||||||
|
LiveNowItemView(item: item)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
AppState.shared.setAppStep(
|
||||||
|
step: .liveDetail(
|
||||||
|
roomId: item.roomId,
|
||||||
|
onClickParticipant: {
|
||||||
|
AppState.shared.isShowPlayer = false
|
||||||
|
onClickParticipant(item.roomId)
|
||||||
|
},
|
||||||
|
onClickReservation: {},
|
||||||
|
onClickStart: {
|
||||||
|
},
|
||||||
|
onClickCancel: {
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
}
|
||||||
|
.padding(.top, 28.3)
|
||||||
|
} else {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Image("ic_no_item")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 60, height: 60)
|
||||||
|
|
||||||
|
Text("🙀지금 참여가능한 라이브가 없습니다.\n직접 라이브를 만들어 보세요!")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 10.7))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.top, 8)
|
||||||
|
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Image("ic_plus_no_bg")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 33.3, height: 33.3, alignment: .center)
|
||||||
|
|
||||||
|
Text("라이브 만들기")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
}
|
||||||
|
.frame(width: 200, height: 33.3, alignment: .center)
|
||||||
|
.background(Color(hex: "9970ff"))
|
||||||
|
.cornerRadius(4.7)
|
||||||
|
.padding(.top, 10.7)
|
||||||
|
.onTapGesture { onTapCreateLive() }
|
||||||
|
}
|
||||||
|
.padding(.vertical, 16.7)
|
||||||
|
.frame(width: screenSize().width - 26.7)
|
||||||
|
.background(Color(hex: "2b2635"))
|
||||||
|
.cornerRadius(4.7)
|
||||||
|
.padding(.top, 28.3)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
//
|
||||||
|
// DateWithWeekDaySymbol.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct DateWithWeekDaySymbol {
|
||||||
|
let date: String
|
||||||
|
let dayOfMonth: String
|
||||||
|
let weekDaySymbol: String
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
//
|
||||||
|
// LiveReservationAllItemView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
struct LiveReservationAllItemView: View {
|
||||||
|
|
||||||
|
let item: GetRoomListResponse
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 13.3) {
|
||||||
|
HStack(spacing: 20) {
|
||||||
|
ZStack(alignment: .topLeading) {
|
||||||
|
KFImage(URL(string: item.coverImageUrl))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 80, height: 116.7, alignment: .top)
|
||||||
|
.cornerRadius(4.7)
|
||||||
|
.clipped()
|
||||||
|
|
||||||
|
if item.isAdult {
|
||||||
|
Text("19")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 11.3))
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
.padding(4)
|
||||||
|
.background(Color(hex: "e53621"))
|
||||||
|
.cornerRadius(20)
|
||||||
|
.padding(.top, 3.3)
|
||||||
|
.padding(.leading, 3.3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(alignment: .top, spacing: 0) {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
Text(item.beginDateTime)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 9.3))
|
||||||
|
.foregroundColor(Color(hex: "ffd300"))
|
||||||
|
|
||||||
|
Text(item.managerNickname)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 11.3))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
.padding(.top, 10)
|
||||||
|
|
||||||
|
Text(item.title)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 15.3))
|
||||||
|
.foregroundColor(Color(hex: "e2e2e2"))
|
||||||
|
.lineLimit(2)
|
||||||
|
.padding(.top, 10)
|
||||||
|
.padding(.trailing, 20)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if item.isReservation {
|
||||||
|
Text("예약완료")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 11.3))
|
||||||
|
.foregroundColor(Color(hex: "d2d2d2"))
|
||||||
|
.padding(.horizontal, 7)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(Color(hex: "533d89"))
|
||||||
|
.cornerRadius(10)
|
||||||
|
} else {
|
||||||
|
Text(item.price > 0 ? "\(item.price)캔" : "무료")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "e2e2e2").opacity(0.49))
|
||||||
|
.padding(.bottom, 6.7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if item.isPrivateRoom {
|
||||||
|
Image("ic_lock")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 6.7)
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle()
|
||||||
|
.foregroundColor(Color(hex: "909090").opacity(0.5))
|
||||||
|
.frame(width: screenSize().width - 26.7, height: 1)
|
||||||
|
}
|
||||||
|
.frame(width: screenSize().width - 26.7, height: 130, alignment: .center)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,125 @@
|
||||||
|
//
|
||||||
|
// LiveReservationAllView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct LiveReservationAllView: View {
|
||||||
|
|
||||||
|
@ObservedObject var viewModel = LiveViewModel()
|
||||||
|
@StateObject var liveAllViewModel = LiveAllViewModel()
|
||||||
|
|
||||||
|
let onClickReservation: (Int) -> Void
|
||||||
|
let onClickStart: (Int) -> Void
|
||||||
|
let onClickCancel: () -> Void
|
||||||
|
let onTapCreateLive: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
|
GeometryReader { proxy in
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
DetailNavigationBar(title: "라이브, 예약 캘린더")
|
||||||
|
|
||||||
|
WeekCalendarView { date in
|
||||||
|
viewModel.selectedDateString = date
|
||||||
|
}
|
||||||
|
.padding(.top, 20)
|
||||||
|
|
||||||
|
if viewModel.liveReservationItems.count > 0 {
|
||||||
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
|
LazyVStack(spacing: 13.3) {
|
||||||
|
ForEach(0..<viewModel.liveReservationItems.count, id: \.self) { index in
|
||||||
|
let item = viewModel.liveReservationItems[index]
|
||||||
|
|
||||||
|
LiveReservationAllItemView(item: item)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
self.liveAllViewModel.selectedRoomId = item.roomId
|
||||||
|
withAnimation {
|
||||||
|
self.liveAllViewModel.isShowLiveDetail = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if index == viewModel.liveNowItems.count - 1 {
|
||||||
|
viewModel.getLiveReservationList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 16.7)
|
||||||
|
} else {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Image("ic_no_item")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 60, height: 60)
|
||||||
|
|
||||||
|
Text("지금 예약중인 라이브가 없습니다.\n직접 라이브를 만들어 보세요!")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 10.7))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.top, 8)
|
||||||
|
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Image("ic_plus_no_bg")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 33.3, height: 33.3, alignment: .center)
|
||||||
|
|
||||||
|
Text("라이브 만들기")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
}
|
||||||
|
.frame(width: 200, height: 33.3, alignment: .center)
|
||||||
|
.background(Color(hex: "9970ff"))
|
||||||
|
.cornerRadius(4.7)
|
||||||
|
.padding(.top, 10.7)
|
||||||
|
.onTapGesture {
|
||||||
|
AppState.shared.back()
|
||||||
|
onTapCreateLive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 16.7)
|
||||||
|
.frame(width: screenSize().width - 26.7)
|
||||||
|
.background(Color(hex: "2b2635"))
|
||||||
|
.cornerRadius(4.7)
|
||||||
|
.padding(.top, 28.3)
|
||||||
|
}
|
||||||
|
|
||||||
|
if proxy.safeAreaInsets.bottom > 0 {
|
||||||
|
Rectangle()
|
||||||
|
.foregroundColor(Color.black.opacity(0))
|
||||||
|
.frame(width: screenSize().width, height: 15.3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
if liveAllViewModel.isShowLiveDetail {
|
||||||
|
LiveDetailView(
|
||||||
|
roomId: liveAllViewModel.selectedRoomId,
|
||||||
|
onClickParticipant: {},
|
||||||
|
onClickReservation: {
|
||||||
|
onClickReservation(liveAllViewModel.selectedRoomId)
|
||||||
|
},
|
||||||
|
onClickStart: {
|
||||||
|
onClickStart(liveAllViewModel.selectedRoomId)
|
||||||
|
},
|
||||||
|
onClickCancel: {
|
||||||
|
viewModel.page = 1
|
||||||
|
viewModel.isLast = false
|
||||||
|
viewModel.getLiveReservationList()
|
||||||
|
onClickCancel()
|
||||||
|
},
|
||||||
|
onClickClose: {
|
||||||
|
withAnimation {
|
||||||
|
liveAllViewModel.isShowLiveDetail = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
//
|
||||||
|
// WeekCalendarView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct WeekCalendarView: View {
|
||||||
|
|
||||||
|
@State private var selectedIndex = 0
|
||||||
|
let action: (String) -> Void
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func DateItemView(index: Int, dateWithWeekDaySymbol: DateWithWeekDaySymbol) -> some View {
|
||||||
|
VStack(spacing: 6.7) {
|
||||||
|
Text(dateWithWeekDaySymbol.weekDaySymbol)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 11.3))
|
||||||
|
.foregroundColor(
|
||||||
|
self.selectedIndex == index ?
|
||||||
|
.white :
|
||||||
|
Color(hex: "e2e2e2")
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(dateWithWeekDaySymbol.dayOfMonth)
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 15.3))
|
||||||
|
.foregroundColor(
|
||||||
|
self.selectedIndex == index ?
|
||||||
|
.white :
|
||||||
|
Color(hex: "e2e2e2")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.frame(width: 53.3)
|
||||||
|
.frame(minHeight: 66.7)
|
||||||
|
.background(index == selectedIndex ? Color(hex: "9970ff") : Color.clear)
|
||||||
|
.cornerRadius(6.7)
|
||||||
|
.onTapGesture {
|
||||||
|
if self.selectedIndex != index {
|
||||||
|
self.selectedIndex = index
|
||||||
|
action(dateWithWeekDaySymbol.date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 16.7) {
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
HStack(spacing: 6.7) {
|
||||||
|
ForEach(0..<7, id: \.self) { index in
|
||||||
|
let dateWithWeekDaySymbol = getDateFromCurrent(afterDay: index)
|
||||||
|
DateItemView(
|
||||||
|
index: index,
|
||||||
|
dateWithWeekDaySymbol: dateWithWeekDaySymbol
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.scaledToFit()
|
||||||
|
|
||||||
|
Rectangle()
|
||||||
|
.frame(height: 1)
|
||||||
|
.foregroundColor(Color(hex: "909090").opacity(0.5))
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
self.selectedIndex = 0
|
||||||
|
action(getDateFromCurrent(afterDay: 0).date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getDateFromCurrent(afterDay: Int) -> DateWithWeekDaySymbol {
|
||||||
|
var calendar = Calendar.current
|
||||||
|
calendar.locale = Locale(identifier: String(Locale.preferredLanguages[0].prefix(2)))
|
||||||
|
|
||||||
|
let currentDate = Date()
|
||||||
|
let futureDate = calendar.date(byAdding: .day, value: afterDay, to: currentDate)!
|
||||||
|
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
dateFormatter.dateFormat = "yyyy-MM-dd"
|
||||||
|
|
||||||
|
let dayOfMonthFormatter = DateFormatter()
|
||||||
|
dayOfMonthFormatter.dateFormat = "d"
|
||||||
|
|
||||||
|
let date = dateFormatter.string(from: futureDate)
|
||||||
|
let dayOfMonth = dayOfMonthFormatter.string(from: futureDate)
|
||||||
|
let day = calendar.component(.weekday, from: futureDate) - 1
|
||||||
|
let weekDaySymbol = calendar.shortWeekdaySymbols[day]
|
||||||
|
|
||||||
|
return DateWithWeekDaySymbol(
|
||||||
|
date: date,
|
||||||
|
dayOfMonth: dayOfMonth,
|
||||||
|
weekDaySymbol: weekDaySymbol
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import Foundation
|
||||||
|
|
||||||
struct MakeLiveReservationRequest: Encodable {
|
struct MakeLiveReservationRequest: Encodable {
|
||||||
let roomId: Int
|
let roomId: Int
|
||||||
let password: Int?
|
let password: String?
|
||||||
let container: String = "ios"
|
let container: String = "ios"
|
||||||
let timezone: String = TimeZone.current.identifier
|
let timezone: String = TimeZone.current.identifier
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,16 @@ struct SectionLiveReservationView: View {
|
||||||
Text("전체보기")
|
Text("전체보기")
|
||||||
.font(.custom(Font.light.rawValue, size: 11.3))
|
.font(.custom(Font.light.rawValue, size: 11.3))
|
||||||
.foregroundColor(Color(hex: "bbbbbb"))
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
.onTapGesture {}
|
.onTapGesture {
|
||||||
|
AppState.shared.setAppStep(
|
||||||
|
step: .liveReservationAll(
|
||||||
|
onClickReservation: onClickReservation,
|
||||||
|
onClickStart: onClickStart,
|
||||||
|
onClickCancel: onClickCancel,
|
||||||
|
onTapCreateLive: onTapCreateLive
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 13.3)
|
.padding(.horizontal, 13.3)
|
||||||
|
@ -47,17 +56,37 @@ struct SectionLiveReservationView: View {
|
||||||
if item.managerId == UserDefaults.int(forKey: .userId) {
|
if item.managerId == UserDefaults.int(forKey: .userId) {
|
||||||
MyLiveReservationItemView(item: item, index: index)
|
MyLiveReservationItemView(item: item, index: index)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {}
|
.onTapGesture {
|
||||||
|
AppState.shared.setAppStep(
|
||||||
|
step: .liveDetail(
|
||||||
|
roomId: item.roomId,
|
||||||
|
onClickParticipant: {},
|
||||||
|
onClickReservation: {},
|
||||||
|
onClickStart: { onClickStart(item.roomId) },
|
||||||
|
onClickCancel: onClickCancel
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
LiveReservationItemView(item: item)
|
LiveReservationItemView(item: item)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {}
|
.onTapGesture {
|
||||||
|
AppState.shared.setAppStep(
|
||||||
|
step: .liveDetail(
|
||||||
|
roomId: item.roomId,
|
||||||
|
onClickParticipant: {},
|
||||||
|
onClickReservation: {},
|
||||||
|
onClickStart: { onClickStart(item.roomId) },
|
||||||
|
onClickCancel: onClickCancel
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 13.3)
|
.padding(.horizontal, 13.3)
|
||||||
.frame(width: screenSize().width)
|
.frame(width: screenSize().width)
|
||||||
.padding(.top, 28.3)
|
.padding(.top, 13.3)
|
||||||
} else {
|
} else {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
Image("ic_no_item")
|
Image("ic_no_item")
|
||||||
|
|
|
@ -11,15 +11,13 @@ struct GetRoomDetailResponse: Decodable {
|
||||||
let roomId: Int
|
let roomId: Int
|
||||||
let price: Int
|
let price: Int
|
||||||
let title: String
|
let title: String
|
||||||
let content: String
|
let notice: String
|
||||||
let isPaid: Bool
|
let isPaid: Bool
|
||||||
let isPrivateRoom: Bool
|
let isPrivateRoom: Bool
|
||||||
let isSecretRoom: Bool
|
let password: String?
|
||||||
let password: Int?
|
|
||||||
let tags: [String]
|
let tags: [String]
|
||||||
let channelName: String?
|
let channelName: String?
|
||||||
let beginDateTime: String
|
let beginDateTime: String
|
||||||
let isNotification: Bool
|
|
||||||
let numberOfParticipants: Int
|
let numberOfParticipants: Int
|
||||||
let numberOfParticipantsTotal: Int
|
let numberOfParticipantsTotal: Int
|
||||||
let manager: GetRoomDetailManager
|
let manager: GetRoomDetailManager
|
||||||
|
@ -35,7 +33,7 @@ struct GetRoomDetailManager: Decodable {
|
||||||
let websiteUrl: String?
|
let websiteUrl: String?
|
||||||
let blogUrl: String?
|
let blogUrl: String?
|
||||||
let profileImageUrl: String
|
let profileImageUrl: String
|
||||||
let isCounselor: Bool
|
let isCreator: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GetRoomDetailUser: Decodable, Hashable {
|
struct GetRoomDetailUser: Decodable, Hashable {
|
||||||
|
|
|
@ -0,0 +1,510 @@
|
||||||
|
//
|
||||||
|
// 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) {
|
||||||
|
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)
|
||||||
|
.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)
|
||||||
|
|
||||||
|
Rectangle()
|
||||||
|
.frame(height: 1)
|
||||||
|
.foregroundColor(Color(hex: "909090").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(hex: "909090").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) {
|
||||||
|
Image("btn_big_share")
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.shareRoom(roomId: room.roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
HStack(spacing: 13.3) {
|
||||||
|
Button {
|
||||||
|
viewModel.shareRoom(roomId: room.roomId)
|
||||||
|
} label: {
|
||||||
|
Image("btn_big_share")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
HStack(spacing: 13.3) {
|
||||||
|
Button {
|
||||||
|
viewModel.shareRoom(roomId: room.roomId)
|
||||||
|
} label: {
|
||||||
|
Image("btn_big_share")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
HStack(spacing: 13.3) {
|
||||||
|
Button {
|
||||||
|
viewModel.shareRoom(roomId: room.roomId)
|
||||||
|
} label: {
|
||||||
|
Image("btn_big_share")
|
||||||
|
}
|
||||||
|
|
||||||
|
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(hex: "eeeeee"))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text("\(room.numberOfParticipants)")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
|
||||||
|
Text("/\(room.numberOfParticipantsTotal)")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
}
|
||||||
|
.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(hex: "bbbbbb"))
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 16.7)
|
||||||
|
} else {
|
||||||
|
let userCount = room.numberOfParticipants > 10 ? 10 : room.numberOfParticipants
|
||||||
|
|
||||||
|
HStack(spacing: -13.3) {
|
||||||
|
ForEach(0..<userCount, id: \.self) { index in
|
||||||
|
let user = room.participatingUsers[index]
|
||||||
|
KFImage(URL(string: user.profileImageUrl))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 33.3, height: 33.3, alignment: .top)
|
||||||
|
.clipShape(Circle())
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("\(room.numberOfParticipants)")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
|
||||||
|
Text("/\(room.numberOfParticipantsTotal)")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 22)
|
||||||
|
}
|
||||||
|
|
||||||
|
if room.numberOfParticipants > 0 {
|
||||||
|
HStack(spacing: 6.7) {
|
||||||
|
Image(isExpandParticipantArea ? "ic_suda_detail_top" : "ic_suda_detail_bottom")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
|
||||||
|
Text(isExpandParticipantArea ? "닫기" : "펼쳐보기")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
}
|
||||||
|
.padding(.top, 13.3)
|
||||||
|
.onTapGesture {
|
||||||
|
isExpandParticipantArea.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hideView() {
|
||||||
|
if let close = onClickClose {
|
||||||
|
close()
|
||||||
|
} else {
|
||||||
|
AppState.shared.back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
//
|
||||||
|
// LiveDetailViewModel.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
import FirebaseDynamicLinks
|
||||||
|
|
||||||
|
final class LiveDetailViewModel: ObservableObject {
|
||||||
|
private let repository = LiveRepository()
|
||||||
|
private var subscription = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
@Published var isLoading = false
|
||||||
|
@Published var errorMessage = ""
|
||||||
|
@Published var isShowPopup = false
|
||||||
|
@Published var room: GetRoomDetailResponse? = nil
|
||||||
|
@Published var showDetail = false
|
||||||
|
|
||||||
|
@Published var shareMessage = ""
|
||||||
|
@Published var isShowShareView = false
|
||||||
|
|
||||||
|
func getDetail(roomId: Int) {
|
||||||
|
if !isLoading {
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
repository.getRoomDetail(roomId: roomId)
|
||||||
|
.sink { result in
|
||||||
|
switch result {
|
||||||
|
case .finished:
|
||||||
|
DEBUG_LOG("finish")
|
||||||
|
case .failure(let error):
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
}
|
||||||
|
} receiveValue: { response in
|
||||||
|
let responseData = response.data
|
||||||
|
|
||||||
|
do {
|
||||||
|
let jsonDecoder = JSONDecoder()
|
||||||
|
let decoded = try jsonDecoder.decode(ApiResponse<GetRoomDetailResponse>.self, from: responseData)
|
||||||
|
|
||||||
|
if let data = decoded.data, decoded.success {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.showDetail = true
|
||||||
|
self.room = data
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let message = decoded.message {
|
||||||
|
self.errorMessage = message
|
||||||
|
} else {
|
||||||
|
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
|
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isLoading = false
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func onBack(afterExecute: () -> Void) {
|
||||||
|
showDetail = false
|
||||||
|
afterExecute()
|
||||||
|
}
|
||||||
|
|
||||||
|
func liveCancel(roomId: Int, reason: String, onSuccess: @escaping () -> Void) {
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
repository.cancelRoom(roomId: roomId, reason: reason)
|
||||||
|
.sink { result in
|
||||||
|
switch result {
|
||||||
|
case .finished:
|
||||||
|
DEBUG_LOG("finish")
|
||||||
|
case .failure(let error):
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
}
|
||||||
|
} receiveValue: { response in
|
||||||
|
self.isLoading = false
|
||||||
|
let responseData = response.data
|
||||||
|
|
||||||
|
do {
|
||||||
|
let jsonDecoder = JSONDecoder()
|
||||||
|
let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
|
||||||
|
|
||||||
|
if decoded.success {
|
||||||
|
onSuccess()
|
||||||
|
} else {
|
||||||
|
if let message = decoded.message {
|
||||||
|
self.errorMessage = message
|
||||||
|
} else {
|
||||||
|
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
}
|
||||||
|
|
||||||
|
func shareRoom(roomId: Int) {
|
||||||
|
isLoading = true
|
||||||
|
guard let link = URL(string: "https://sodalive.net/?room_id=\(roomId)") else { return }
|
||||||
|
let dynamicLinksDomainURIPrefix = "https://sodalive.page.link"
|
||||||
|
guard let linkBuilder = DynamicLinkComponents(link: link, domainURIPrefix: dynamicLinksDomainURIPrefix) else {
|
||||||
|
self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요."
|
||||||
|
self.isShowPopup = true
|
||||||
|
isLoading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
linkBuilder.iOSParameters = DynamicLinkIOSParameters(bundleID: "kr.co.vividnext.sodalive")
|
||||||
|
linkBuilder.iOSParameters?.appStoreID = "1630284226"
|
||||||
|
|
||||||
|
linkBuilder.androidParameters = DynamicLinkAndroidParameters(packageName: "kr.co.vividnext.sodalive")
|
||||||
|
|
||||||
|
guard let longDynamicLink = linkBuilder.url else {
|
||||||
|
self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요."
|
||||||
|
self.isShowPopup = true
|
||||||
|
isLoading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DEBUG_LOG("The long URL is: \(longDynamicLink)")
|
||||||
|
|
||||||
|
DynamicLinkComponents.shortenURL(longDynamicLink, options: nil) { [unowned self] url, warnings, error in
|
||||||
|
let shortUrl = url?.absoluteString
|
||||||
|
|
||||||
|
if let liveRoomInfo = self.room {
|
||||||
|
let urlString = shortUrl != nil ? shortUrl! : longDynamicLink.absoluteString
|
||||||
|
if liveRoomInfo.isPrivateRoom {
|
||||||
|
shareMessage = "\(UserDefaults.string(forKey: .nickname))님이 귀하를 소다라이브 비공개라이브에 초대하였습니다.\n" +
|
||||||
|
"※ 라이브 참여: \(urlString)\n" +
|
||||||
|
"(입장 비밀번호: \(liveRoomInfo.password!))"
|
||||||
|
} else {
|
||||||
|
shareMessage = "\(UserDefaults.string(forKey: .nickname))님이 귀하를 소다라이브 공개라이브에 초대하였습니다.\n" +
|
||||||
|
"※ 라이브 참여: \(urlString)"
|
||||||
|
}
|
||||||
|
|
||||||
|
isShowShareView = true
|
||||||
|
} else {
|
||||||
|
self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요."
|
||||||
|
self.isShowPopup = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
//
|
||||||
|
// EditLiveRoomInfoRequest.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct EditLiveRoomInfoRequest: Encodable {
|
||||||
|
let title: String?
|
||||||
|
let notice: String?
|
||||||
|
let numberOfPeople: Int?
|
||||||
|
let beginDateTimeString: String?
|
||||||
|
let timezone: String?
|
||||||
|
}
|
|
@ -0,0 +1,292 @@
|
||||||
|
//
|
||||||
|
// LiveRoomEditView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct LiveRoomEditView: View {
|
||||||
|
|
||||||
|
@StateObject var keyboardHandler = KeyboardHandler()
|
||||||
|
@StateObject var viewModel = LiveRoomEditViewModel()
|
||||||
|
|
||||||
|
@State private var isShowSelectDateView = false
|
||||||
|
@State private var isShowSelectTimeView = false
|
||||||
|
|
||||||
|
let room: GetRoomDetailResponse
|
||||||
|
|
||||||
|
init(room: GetRoomDetailResponse) {
|
||||||
|
UITextView.appearance().backgroundColor = .clear
|
||||||
|
UIScrollView.appearance().bounces = false
|
||||||
|
self.room = room
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
|
ZStack {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
DetailNavigationBar(title: "라이브 수정")
|
||||||
|
|
||||||
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
TitleInputView()
|
||||||
|
.frame(width: screenSize().width - 26.7)
|
||||||
|
.padding(.top, 33.3)
|
||||||
|
|
||||||
|
ContentInputView()
|
||||||
|
.frame(width: screenSize().width - 26.7)
|
||||||
|
.padding(.top, 33.3)
|
||||||
|
|
||||||
|
ReservationDateTimeView(buttonWidth: (screenSize().width - 40) / 2)
|
||||||
|
.padding(.top, 22.7)
|
||||||
|
|
||||||
|
NumberOfPeopleLimitView()
|
||||||
|
.frame(width: screenSize().width - 26.7)
|
||||||
|
.padding(.top, 33.3)
|
||||||
|
|
||||||
|
if !viewModel.isLoading {
|
||||||
|
Text("라이브 수정")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
.frame(width: screenSize().width - 26.7, height: 50)
|
||||||
|
.background(Color(hex: "9970ff"))
|
||||||
|
.cornerRadius(10)
|
||||||
|
.padding(.top, 30)
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.updateLiveRoom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle()
|
||||||
|
.foregroundColor(Color(hex: "222222"))
|
||||||
|
.frame(width: screenSize().width, height: keyboardHandler.keyboardHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isShowSelectDateView {
|
||||||
|
SelectDateView()
|
||||||
|
}
|
||||||
|
|
||||||
|
if isShowSelectTimeView {
|
||||||
|
SelectTimeView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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: 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
viewModel.room = room
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func TitleInputView() -> some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Text("제목")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 16.7))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
.frame(width: screenSize().width, alignment: .leading)
|
||||||
|
|
||||||
|
TextField("라이브 제목을 입력하세요", text: $viewModel.title)
|
||||||
|
.autocapitalization(.none)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
.accentColor(Color(hex: "9970ff"))
|
||||||
|
.keyboardType(.default)
|
||||||
|
.padding(.top, 12)
|
||||||
|
.padding(.horizontal, 6.7)
|
||||||
|
|
||||||
|
Rectangle()
|
||||||
|
.frame(height: 1)
|
||||||
|
.foregroundColor(Color(hex: "909090").opacity(0.7))
|
||||||
|
.padding(.top, 8.3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func ContentInputView() -> some View {
|
||||||
|
VStack(spacing: 13.3) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("공지")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 16.7))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text("\(viewModel.notice.count)자")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "ff5c49")) +
|
||||||
|
Text(" / 1000자")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "777777"))
|
||||||
|
}
|
||||||
|
|
||||||
|
TextViewWrapper(
|
||||||
|
text: $viewModel.notice,
|
||||||
|
placeholder: viewModel.placeholder,
|
||||||
|
textColorHex: "eeeeee",
|
||||||
|
backgroundColorHex: "222222"
|
||||||
|
)
|
||||||
|
.frame(width: screenSize().width - 26.7, height: 133.3)
|
||||||
|
.cornerRadius(6.7)
|
||||||
|
.padding(.top, 13.3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func ReservationDateTimeView(buttonWidth: CGFloat) -> some View {
|
||||||
|
HStack(spacing: 13.3) {
|
||||||
|
VStack(alignment: .leading, spacing: 6.7) {
|
||||||
|
Text("예약 날짜")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
hideKeyboard()
|
||||||
|
self.isShowSelectDateView = true
|
||||||
|
}) {
|
||||||
|
Text(viewModel.reservationDateString)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
.frame(width: buttonWidth, height: 48.7)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 6.7)
|
||||||
|
.stroke(Color(hex: "9970ff"), lineWidth: 1.3)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 6.7) {
|
||||||
|
Text("예약 시간")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
hideKeyboard()
|
||||||
|
self.isShowSelectTimeView = true
|
||||||
|
}) {
|
||||||
|
Text(viewModel.reservationTimeString)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
.frame(width: buttonWidth, height: 48.7)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 6.7)
|
||||||
|
.stroke(Color(hex: "9970ff"), lineWidth: 1.3)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: screenSize().width)
|
||||||
|
.padding(.vertical, 13.3)
|
||||||
|
.background(Color(hex: "222222"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func NumberOfPeopleLimitView() -> some View {
|
||||||
|
VStack(spacing: 13.3) {
|
||||||
|
Text("참여인원 설정")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 16.7))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
.frame(width: screenSize().width - 26.7, alignment: .leading)
|
||||||
|
|
||||||
|
TextField("최대 인원 999명", text: $viewModel.numberOfPeople)
|
||||||
|
.autocapitalization(.none)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
.accentColor(Color(hex: "9970ff"))
|
||||||
|
.keyboardType(.numberPad)
|
||||||
|
.padding(.vertical, 15.7)
|
||||||
|
.frame(width: screenSize().width - 26.7, alignment: .center)
|
||||||
|
.background(Color(hex: "222222"))
|
||||||
|
.cornerRadius(6.7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func SelectDateView() -> some View {
|
||||||
|
GeometryReader { proxy in
|
||||||
|
ZStack {
|
||||||
|
Color
|
||||||
|
.black
|
||||||
|
.opacity(0.5)
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
DatePicker("", selection: $viewModel.reservationDate, in: Date()..., displayedComponents: .date)
|
||||||
|
.datePickerStyle(WheelDatePickerStyle())
|
||||||
|
.labelsHidden()
|
||||||
|
.environment(\.locale, Locale.init(identifier: "ko"))
|
||||||
|
.frame(width: proxy.size.width)
|
||||||
|
|
||||||
|
Button(action: { self.isShowSelectDateView = false }) {
|
||||||
|
Text("확인")
|
||||||
|
.font(.system(size: 16))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
.frame(width: proxy.size.width - 53.4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(Color(hex: "222222"))
|
||||||
|
.cornerRadius(6.7)
|
||||||
|
}
|
||||||
|
.frame(width: proxy.size.width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func SelectTimeView() -> some View {
|
||||||
|
GeometryReader { proxy in
|
||||||
|
ZStack {
|
||||||
|
Color
|
||||||
|
.black
|
||||||
|
.opacity(0.5)
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
DatePicker("", selection: $viewModel.reservationTime, displayedComponents: .hourAndMinute)
|
||||||
|
.datePickerStyle(WheelDatePickerStyle())
|
||||||
|
.labelsHidden()
|
||||||
|
.environment(\.locale, Locale.init(identifier: "ko"))
|
||||||
|
.frame(width: proxy.size.width - 53.4)
|
||||||
|
|
||||||
|
Button(action: { self.isShowSelectTimeView = false }) {
|
||||||
|
Text("확인")
|
||||||
|
.font(.system(size: 16))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
.frame(width: proxy.size.width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(Color(hex: "222222"))
|
||||||
|
.cornerRadius(6.7)
|
||||||
|
}
|
||||||
|
.frame(width: proxy.size.width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,178 @@
|
||||||
|
//
|
||||||
|
// LiveRoomEditViewModel.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Moya
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
final class LiveRoomEditViewModel: ObservableObject {
|
||||||
|
|
||||||
|
@Published var isLoading = false
|
||||||
|
|
||||||
|
@Published var title: String = ""
|
||||||
|
@Published var notice: String = "" {
|
||||||
|
didSet {
|
||||||
|
if notice.count > 1000 {
|
||||||
|
notice = String(notice.prefix(1000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Published var numberOfPeople = ""
|
||||||
|
@Published var reservationDateString: String = ""
|
||||||
|
@Published var reservationTimeString: String = ""
|
||||||
|
|
||||||
|
@Published var errorMessage = ""
|
||||||
|
@Published var isShowPopup = false
|
||||||
|
|
||||||
|
private let repository = LiveRepository()
|
||||||
|
private var subscription = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
var reservationDate = Date() {
|
||||||
|
didSet {
|
||||||
|
reservationDateString = reservationDate.convertDateFormat(dateFormat: "yyyy.MM.dd")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var reservationTime = Date() {
|
||||||
|
didSet {
|
||||||
|
reservationTimeString = reservationTime.convertDateFormat(dateFormat: "a hh:mm")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let placeholder = "라이브 공지를 입력하세요"
|
||||||
|
|
||||||
|
var room: GetRoomDetailResponse? = nil {
|
||||||
|
didSet {
|
||||||
|
isLoading = true
|
||||||
|
title = room!.title
|
||||||
|
notice = room!.notice
|
||||||
|
numberOfPeople = String(room!.numberOfParticipantsTotal)
|
||||||
|
|
||||||
|
let fromFormatter = DateFormatter()
|
||||||
|
fromFormatter.dateFormat = "yyyy.MM.dd EEE hh:mm a"
|
||||||
|
fromFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||||
|
|
||||||
|
reservationDate = fromFormatter.date(from: room!.beginDateTime)!
|
||||||
|
reservationTime = fromFormatter.date(from: room!.beginDateTime)!
|
||||||
|
|
||||||
|
let beginDate = reservationDate.convertDateFormat(dateFormat: "yyyy-MM-dd")
|
||||||
|
let beginTime = reservationTime.convertDateFormat(dateFormat: "HH:mm")
|
||||||
|
|
||||||
|
beginDateTimeStr = "\(beginDate) \(beginTime)"
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var beginDateTimeStr: String = ""
|
||||||
|
|
||||||
|
func updateLiveRoom() {
|
||||||
|
if let room = room, !isLoading && validate() {
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
let beginDate = reservationDate.convertDateFormat(dateFormat: "yyyy-MM-dd")
|
||||||
|
let beginTime = reservationTime.convertDateFormat(dateFormat: "HH:mm")
|
||||||
|
let beginDateTime = "\(beginDate) \(beginTime)"
|
||||||
|
|
||||||
|
let request = EditLiveRoomInfoRequest(
|
||||||
|
title: room.title != title ? title : nil,
|
||||||
|
notice: room.notice != notice ? notice : nil,
|
||||||
|
numberOfPeople: room.numberOfParticipantsTotal != Int(numberOfPeople)! ? Int(numberOfPeople)! : nil,
|
||||||
|
beginDateTimeString: beginDateTimeStr != beginDateTime ? beginDateTime : nil,
|
||||||
|
timezone: TimeZone.current.identifier
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
request.title == nil &&
|
||||||
|
request.notice == nil &&
|
||||||
|
request.numberOfPeople == nil &&
|
||||||
|
request.beginDateTimeString == nil
|
||||||
|
) {
|
||||||
|
self.errorMessage = "변경사항이 없습니다."
|
||||||
|
self.isShowPopup = true
|
||||||
|
isLoading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var multipartData = [MultipartFormData]()
|
||||||
|
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
encoder.outputFormatting = .withoutEscapingSlashes
|
||||||
|
let jsonData = try? encoder.encode(request)
|
||||||
|
|
||||||
|
if let jsonData = jsonData {
|
||||||
|
multipartData.append(MultipartFormData(provider: .data(jsonData), name: "request"))
|
||||||
|
|
||||||
|
repository.editLiveRoomInfo(roomId: room.roomId, parameters: multipartData)
|
||||||
|
.sink { result in
|
||||||
|
switch result {
|
||||||
|
case .finished:
|
||||||
|
DEBUG_LOG("finish")
|
||||||
|
case .failure(let error):
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
}
|
||||||
|
} receiveValue: { response in
|
||||||
|
self.isLoading = false
|
||||||
|
let responseData = response.data
|
||||||
|
|
||||||
|
do {
|
||||||
|
let jsonDecoder = JSONDecoder()
|
||||||
|
let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
|
||||||
|
|
||||||
|
if decoded.success {
|
||||||
|
self.errorMessage = "라이브 정보가 수정되었습니다."
|
||||||
|
self.isShowPopup = true
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
|
AppState.shared.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if let message = decoded.message {
|
||||||
|
self.errorMessage = message
|
||||||
|
} else {
|
||||||
|
self.errorMessage = "라이브 정보를 수정 하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
self.errorMessage = "라이브 정보를 수정 하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
} else {
|
||||||
|
self.errorMessage = "라이브 정보를 수정 하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
self.isShowPopup = true
|
||||||
|
self.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func validate() -> Bool {
|
||||||
|
if title.trimmingCharacters(in: .whitespaces).isEmpty {
|
||||||
|
self.errorMessage = "제목을 입력해 주세요."
|
||||||
|
self.isShowPopup = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let notice = notice.trimmingCharacters(in: .whitespacesAndNewlines) != placeholder ? notice : ""
|
||||||
|
if notice.isEmpty && notice.count < 5 {
|
||||||
|
self.errorMessage = "공지를 5자 이상 입력해주세요."
|
||||||
|
self.isShowPopup = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let numberOfPeople = Int(numberOfPeople), (numberOfPeople >= 3 && numberOfPeople <= 999) else {
|
||||||
|
self.errorMessage = "인원을 3~999명 사이로 입력해주세요."
|
||||||
|
self.isShowPopup = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,5 +10,5 @@ import Foundation
|
||||||
struct EnterOrQuitLiveRoomRequest: Encodable {
|
struct EnterOrQuitLiveRoomRequest: Encodable {
|
||||||
let roomId: Int
|
let roomId: Int
|
||||||
let container: String = "ios"
|
let container: String = "ios"
|
||||||
var password: Int? = nil
|
var password: String? = nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,259 @@
|
||||||
|
// Generated using the ObjectBox Swift Generator — https://objectbox.io
|
||||||
|
// DO NOT EDIT
|
||||||
|
|
||||||
|
// swiftlint:disable all
|
||||||
|
import ObjectBox
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// MARK: - Entity metadata
|
||||||
|
|
||||||
|
|
||||||
|
extension PlaybackTracking: ObjectBox.__EntityRelatable {
|
||||||
|
internal typealias EntityType = PlaybackTracking
|
||||||
|
|
||||||
|
internal var _id: EntityId<PlaybackTracking> {
|
||||||
|
return EntityId<PlaybackTracking>(self.id.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PlaybackTracking: ObjectBox.EntityInspectable {
|
||||||
|
internal typealias EntityBindingType = PlaybackTrackingBinding
|
||||||
|
|
||||||
|
/// Generated metadata used by ObjectBox to persist the entity.
|
||||||
|
internal static var entityInfo = ObjectBox.EntityInfo(name: "PlaybackTracking", id: 1)
|
||||||
|
|
||||||
|
internal static var entityBinding = EntityBindingType()
|
||||||
|
|
||||||
|
fileprivate static func buildEntity(modelBuilder: ObjectBox.ModelBuilder) throws {
|
||||||
|
let entityBuilder = try modelBuilder.entityBuilder(for: PlaybackTracking.self, id: 1, uid: 1902306876074642688)
|
||||||
|
try entityBuilder.addProperty(name: "id", type: PropertyType.long, flags: [.id], id: 1, uid: 3822545071117514752)
|
||||||
|
try entityBuilder.addProperty(name: "audioContentId", type: PropertyType.long, id: 2, uid: 6201823391120048640)
|
||||||
|
try entityBuilder.addProperty(name: "totalDuration", type: PropertyType.long, id: 3, uid: 8353299921632812032)
|
||||||
|
try entityBuilder.addProperty(name: "startPosition", type: PropertyType.long, id: 4, uid: 3188699482915899648)
|
||||||
|
try entityBuilder.addProperty(name: "isFree", type: PropertyType.bool, id: 5, uid: 2487054984108217856)
|
||||||
|
try entityBuilder.addProperty(name: "isPreview", type: PropertyType.bool, id: 6, uid: 5106135603734636032)
|
||||||
|
try entityBuilder.addProperty(name: "endPosition", type: PropertyType.long, id: 7, uid: 8116657363890041600)
|
||||||
|
try entityBuilder.addProperty(name: "playDateTime", type: PropertyType.string, id: 8, uid: 8837430652093702400)
|
||||||
|
|
||||||
|
try entityBuilder.lastProperty(id: 8, uid: 8837430652093702400)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PlaybackTracking {
|
||||||
|
/// Generated entity property information.
|
||||||
|
///
|
||||||
|
/// You may want to use this in queries to specify fetch conditions, for example:
|
||||||
|
///
|
||||||
|
/// box.query { PlaybackTracking.id == myId }
|
||||||
|
internal static var id: Property<PlaybackTracking, Id, Id> { return Property<PlaybackTracking, Id, Id>(propertyId: 1, isPrimaryKey: true) }
|
||||||
|
/// Generated entity property information.
|
||||||
|
///
|
||||||
|
/// You may want to use this in queries to specify fetch conditions, for example:
|
||||||
|
///
|
||||||
|
/// box.query { PlaybackTracking.audioContentId > 1234 }
|
||||||
|
internal static var audioContentId: Property<PlaybackTracking, Int, Void> { return Property<PlaybackTracking, Int, Void>(propertyId: 2, isPrimaryKey: false) }
|
||||||
|
/// Generated entity property information.
|
||||||
|
///
|
||||||
|
/// You may want to use this in queries to specify fetch conditions, for example:
|
||||||
|
///
|
||||||
|
/// box.query { PlaybackTracking.totalDuration > 1234 }
|
||||||
|
internal static var totalDuration: Property<PlaybackTracking, Int, Void> { return Property<PlaybackTracking, Int, Void>(propertyId: 3, isPrimaryKey: false) }
|
||||||
|
/// Generated entity property information.
|
||||||
|
///
|
||||||
|
/// You may want to use this in queries to specify fetch conditions, for example:
|
||||||
|
///
|
||||||
|
/// box.query { PlaybackTracking.startPosition > 1234 }
|
||||||
|
internal static var startPosition: Property<PlaybackTracking, Int, Void> { return Property<PlaybackTracking, Int, Void>(propertyId: 4, isPrimaryKey: false) }
|
||||||
|
/// Generated entity property information.
|
||||||
|
///
|
||||||
|
/// You may want to use this in queries to specify fetch conditions, for example:
|
||||||
|
///
|
||||||
|
/// box.query { PlaybackTracking.isFree == true }
|
||||||
|
internal static var isFree: Property<PlaybackTracking, Bool, Void> { return Property<PlaybackTracking, Bool, Void>(propertyId: 5, isPrimaryKey: false) }
|
||||||
|
/// Generated entity property information.
|
||||||
|
///
|
||||||
|
/// You may want to use this in queries to specify fetch conditions, for example:
|
||||||
|
///
|
||||||
|
/// box.query { PlaybackTracking.isPreview == true }
|
||||||
|
internal static var isPreview: Property<PlaybackTracking, Bool, Void> { return Property<PlaybackTracking, Bool, Void>(propertyId: 6, isPrimaryKey: false) }
|
||||||
|
/// Generated entity property information.
|
||||||
|
///
|
||||||
|
/// You may want to use this in queries to specify fetch conditions, for example:
|
||||||
|
///
|
||||||
|
/// box.query { PlaybackTracking.endPosition > 1234 }
|
||||||
|
internal static var endPosition: Property<PlaybackTracking, Int?, Void> { return Property<PlaybackTracking, Int?, Void>(propertyId: 7, isPrimaryKey: false) }
|
||||||
|
/// Generated entity property information.
|
||||||
|
///
|
||||||
|
/// You may want to use this in queries to specify fetch conditions, for example:
|
||||||
|
///
|
||||||
|
/// box.query { PlaybackTracking.playDateTime.startsWith("X") }
|
||||||
|
internal static var playDateTime: Property<PlaybackTracking, String, Void> { return Property<PlaybackTracking, String, Void>(propertyId: 8, isPrimaryKey: false) }
|
||||||
|
|
||||||
|
fileprivate func __setId(identifier: ObjectBox.Id) {
|
||||||
|
self.id = Id(identifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ObjectBox.Property where E == PlaybackTracking {
|
||||||
|
/// Generated entity property information.
|
||||||
|
///
|
||||||
|
/// You may want to use this in queries to specify fetch conditions, for example:
|
||||||
|
///
|
||||||
|
/// box.query { .id == myId }
|
||||||
|
|
||||||
|
internal static var id: Property<PlaybackTracking, Id, Id> { return Property<PlaybackTracking, Id, Id>(propertyId: 1, isPrimaryKey: true) }
|
||||||
|
|
||||||
|
/// Generated entity property information.
|
||||||
|
///
|
||||||
|
/// You may want to use this in queries to specify fetch conditions, for example:
|
||||||
|
///
|
||||||
|
/// box.query { .audioContentId > 1234 }
|
||||||
|
|
||||||
|
internal static var audioContentId: Property<PlaybackTracking, Int, Void> { return Property<PlaybackTracking, Int, Void>(propertyId: 2, isPrimaryKey: false) }
|
||||||
|
|
||||||
|
/// Generated entity property information.
|
||||||
|
///
|
||||||
|
/// You may want to use this in queries to specify fetch conditions, for example:
|
||||||
|
///
|
||||||
|
/// box.query { .totalDuration > 1234 }
|
||||||
|
|
||||||
|
internal static var totalDuration: Property<PlaybackTracking, Int, Void> { return Property<PlaybackTracking, Int, Void>(propertyId: 3, isPrimaryKey: false) }
|
||||||
|
|
||||||
|
/// Generated entity property information.
|
||||||
|
///
|
||||||
|
/// You may want to use this in queries to specify fetch conditions, for example:
|
||||||
|
///
|
||||||
|
/// box.query { .startPosition > 1234 }
|
||||||
|
|
||||||
|
internal static var startPosition: Property<PlaybackTracking, Int, Void> { return Property<PlaybackTracking, Int, Void>(propertyId: 4, isPrimaryKey: false) }
|
||||||
|
|
||||||
|
/// Generated entity property information.
|
||||||
|
///
|
||||||
|
/// You may want to use this in queries to specify fetch conditions, for example:
|
||||||
|
///
|
||||||
|
/// box.query { .isFree == true }
|
||||||
|
|
||||||
|
internal static var isFree: Property<PlaybackTracking, Bool, Void> { return Property<PlaybackTracking, Bool, Void>(propertyId: 5, isPrimaryKey: false) }
|
||||||
|
|
||||||
|
/// Generated entity property information.
|
||||||
|
///
|
||||||
|
/// You may want to use this in queries to specify fetch conditions, for example:
|
||||||
|
///
|
||||||
|
/// box.query { .isPreview == true }
|
||||||
|
|
||||||
|
internal static var isPreview: Property<PlaybackTracking, Bool, Void> { return Property<PlaybackTracking, Bool, Void>(propertyId: 6, isPrimaryKey: false) }
|
||||||
|
|
||||||
|
/// Generated entity property information.
|
||||||
|
///
|
||||||
|
/// You may want to use this in queries to specify fetch conditions, for example:
|
||||||
|
///
|
||||||
|
/// box.query { .endPosition > 1234 }
|
||||||
|
|
||||||
|
internal static var endPosition: Property<PlaybackTracking, Int?, Void> { return Property<PlaybackTracking, Int?, Void>(propertyId: 7, isPrimaryKey: false) }
|
||||||
|
|
||||||
|
/// Generated entity property information.
|
||||||
|
///
|
||||||
|
/// You may want to use this in queries to specify fetch conditions, for example:
|
||||||
|
///
|
||||||
|
/// box.query { .playDateTime.startsWith("X") }
|
||||||
|
|
||||||
|
internal static var playDateTime: Property<PlaybackTracking, String, Void> { return Property<PlaybackTracking, String, Void>(propertyId: 8, isPrimaryKey: false) }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Generated service type to handle persisting and reading entity data. Exposed through `PlaybackTracking.EntityBindingType`.
|
||||||
|
internal class PlaybackTrackingBinding: ObjectBox.EntityBinding {
|
||||||
|
internal typealias EntityType = PlaybackTracking
|
||||||
|
internal typealias IdType = Id
|
||||||
|
|
||||||
|
internal required init() {}
|
||||||
|
|
||||||
|
internal func generatorBindingVersion() -> Int { 1 }
|
||||||
|
|
||||||
|
internal func setEntityIdUnlessStruct(of entity: EntityType, to entityId: ObjectBox.Id) {
|
||||||
|
entity.__setId(identifier: entityId)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal func entityId(of entity: EntityType) -> ObjectBox.Id {
|
||||||
|
return entity.id.value
|
||||||
|
}
|
||||||
|
|
||||||
|
internal func collect(fromEntity entity: EntityType, id: ObjectBox.Id,
|
||||||
|
propertyCollector: ObjectBox.FlatBufferBuilder, store: ObjectBox.Store) throws {
|
||||||
|
let propertyOffset_playDateTime = propertyCollector.prepare(string: entity.playDateTime)
|
||||||
|
|
||||||
|
propertyCollector.collect(id, at: 2 + 2 * 1)
|
||||||
|
propertyCollector.collect(entity.audioContentId, at: 2 + 2 * 2)
|
||||||
|
propertyCollector.collect(entity.totalDuration, at: 2 + 2 * 3)
|
||||||
|
propertyCollector.collect(entity.startPosition, at: 2 + 2 * 4)
|
||||||
|
propertyCollector.collect(entity.isFree, at: 2 + 2 * 5)
|
||||||
|
propertyCollector.collect(entity.isPreview, at: 2 + 2 * 6)
|
||||||
|
propertyCollector.collect(entity.endPosition, at: 2 + 2 * 7)
|
||||||
|
propertyCollector.collect(dataOffset: propertyOffset_playDateTime, at: 2 + 2 * 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal func createEntity(entityReader: ObjectBox.FlatBufferReader, store: ObjectBox.Store) -> EntityType {
|
||||||
|
let entity = PlaybackTracking()
|
||||||
|
|
||||||
|
entity.id = entityReader.read(at: 2 + 2 * 1)
|
||||||
|
entity.audioContentId = entityReader.read(at: 2 + 2 * 2)
|
||||||
|
entity.totalDuration = entityReader.read(at: 2 + 2 * 3)
|
||||||
|
entity.startPosition = entityReader.read(at: 2 + 2 * 4)
|
||||||
|
entity.isFree = entityReader.read(at: 2 + 2 * 5)
|
||||||
|
entity.isPreview = entityReader.read(at: 2 + 2 * 6)
|
||||||
|
entity.endPosition = entityReader.read(at: 2 + 2 * 7)
|
||||||
|
entity.playDateTime = entityReader.read(at: 2 + 2 * 8)
|
||||||
|
|
||||||
|
return entity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Helper function that allows calling Enum(rawValue: value) with a nil value, which will return nil.
|
||||||
|
fileprivate func optConstruct<T: RawRepresentable>(_ type: T.Type, rawValue: T.RawValue?) -> T? {
|
||||||
|
guard let rawValue = rawValue else { return nil }
|
||||||
|
return T(rawValue: rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Store setup
|
||||||
|
|
||||||
|
fileprivate func cModel() throws -> OpaquePointer {
|
||||||
|
let modelBuilder = try ObjectBox.ModelBuilder()
|
||||||
|
try PlaybackTracking.buildEntity(modelBuilder: modelBuilder)
|
||||||
|
modelBuilder.lastEntity(id: 1, uid: 1902306876074642688)
|
||||||
|
return modelBuilder.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ObjectBox.Store {
|
||||||
|
/// A store with a fully configured model. Created by the code generator with your model's metadata in place.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - directoryPath: The directory path in which ObjectBox places its database files for this store.
|
||||||
|
/// - maxDbSizeInKByte: Limit of on-disk space for the database files. Default is `1024 * 1024` (1 GiB).
|
||||||
|
/// - fileMode: UNIX-style bit mask used for the database files; default is `0o644`.
|
||||||
|
/// Note: directories become searchable if the "read" or "write" permission is set (e.g. 0640 becomes 0750).
|
||||||
|
/// - maxReaders: The maximum number of readers.
|
||||||
|
/// "Readers" are a finite resource for which we need to define a maximum number upfront.
|
||||||
|
/// The default value is enough for most apps and usually you can ignore it completely.
|
||||||
|
/// However, if you get the maxReadersExceeded error, you should verify your
|
||||||
|
/// threading. For each thread, ObjectBox uses multiple readers. Their number (per thread) depends
|
||||||
|
/// on number of types, relations, and usage patterns. Thus, if you are working with many threads
|
||||||
|
/// (e.g. in a server-like scenario), it can make sense to increase the maximum number of readers.
|
||||||
|
/// Note: The internal default is currently around 120.
|
||||||
|
/// So when hitting this limit, try values around 200-500.
|
||||||
|
/// - important: This initializer is created by the code generator. If you only see the internal `init(model:...)`
|
||||||
|
/// initializer, trigger code generation by building your project.
|
||||||
|
internal convenience init(directoryPath: String, maxDbSizeInKByte: UInt64 = 1024 * 1024,
|
||||||
|
fileMode: UInt32 = 0o644, maxReaders: UInt32 = 0, readOnly: Bool = false) throws {
|
||||||
|
try self.init(
|
||||||
|
model: try cModel(),
|
||||||
|
directory: directoryPath,
|
||||||
|
maxDbSizeInKByte: maxDbSizeInKByte,
|
||||||
|
fileMode: fileMode,
|
||||||
|
maxReaders: maxReaders,
|
||||||
|
readOnly: readOnly)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// swiftlint:enable all
|
|
@ -0,0 +1,57 @@
|
||||||
|
// Build your project to run Sourcery and create current contents for this file
|
||||||
|
|
||||||
|
// Generated using the ObjectBox Swift Generator — https://objectbox.io
|
||||||
|
// DO NOT EDIT
|
||||||
|
|
||||||
|
// swiftlint:disable all
|
||||||
|
import ObjectBox
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// MARK: - Entity metadata
|
||||||
|
|
||||||
|
/// Helper function that allows calling Enum(rawValue: value) with a nil value, which will return nil.
|
||||||
|
fileprivate func optConstruct<T: RawRepresentable>(_ type: T.Type, rawValue: T.RawValue?) -> T? {
|
||||||
|
guard let rawValue = rawValue else { return nil }
|
||||||
|
return T(rawValue: rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Store setup
|
||||||
|
|
||||||
|
fileprivate func cModel() throws -> OpaquePointer {
|
||||||
|
let modelBuilder = try ObjectBox.ModelBuilder()
|
||||||
|
modelBuilder.lastEntity(id: 0, uid: 0)
|
||||||
|
return modelBuilder.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ObjectBox.Store {
|
||||||
|
/// A store with a fully configured model. Created by the code generator with your model's metadata in place.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - directoryPath: The directory path in which ObjectBox places its database files for this store.
|
||||||
|
/// - maxDbSizeInKByte: Limit of on-disk space for the database files. Default is `1024 * 1024` (1 GiB).
|
||||||
|
/// - fileMode: UNIX-style bit mask used for the database files; default is `0o644`.
|
||||||
|
/// Note: directories become searchable if the "read" or "write" permission is set (e.g. 0640 becomes 0750).
|
||||||
|
/// - maxReaders: The maximum number of readers.
|
||||||
|
/// "Readers" are a finite resource for which we need to define a maximum number upfront.
|
||||||
|
/// The default value is enough for most apps and usually you can ignore it completely.
|
||||||
|
/// However, if you get the maxReadersExceeded error, you should verify your
|
||||||
|
/// threading. For each thread, ObjectBox uses multiple readers. Their number (per thread) depends
|
||||||
|
/// on number of types, relations, and usage patterns. Thus, if you are working with many threads
|
||||||
|
/// (e.g. in a server-like scenario), it can make sense to increase the maximum number of readers.
|
||||||
|
/// Note: The internal default is currently around 120.
|
||||||
|
/// So when hitting this limit, try values around 200-500.
|
||||||
|
/// - important: This initializer is created by the code generator. If you only see the internal `init(model:...)`
|
||||||
|
/// initializer, trigger code generation by building your project.
|
||||||
|
internal convenience init(directoryPath: String, maxDbSizeInKByte: UInt64 = 1024 * 1024,
|
||||||
|
fileMode: UInt32 = 0o644, maxReaders: UInt32 = 0, readOnly: Bool = false) throws {
|
||||||
|
try self.init(
|
||||||
|
model: try cModel(),
|
||||||
|
directory: directoryPath,
|
||||||
|
maxDbSizeInKByte: maxDbSizeInKByte,
|
||||||
|
fileMode: fileMode,
|
||||||
|
maxReaders: maxReaders,
|
||||||
|
readOnly: readOnly)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// swiftlint:enable all
|
|
@ -0,0 +1,67 @@
|
||||||
|
{
|
||||||
|
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
|
||||||
|
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
|
||||||
|
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"id": "1:1902306876074642688",
|
||||||
|
"lastPropertyId": "8:8837430652093702400",
|
||||||
|
"name": "PlaybackTracking",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"flags": 1,
|
||||||
|
"id": "1:3822545071117514752",
|
||||||
|
"name": "id",
|
||||||
|
"type": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2:6201823391120048640",
|
||||||
|
"name": "audioContentId",
|
||||||
|
"type": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3:8353299921632812032",
|
||||||
|
"name": "totalDuration",
|
||||||
|
"type": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4:3188699482915899648",
|
||||||
|
"name": "startPosition",
|
||||||
|
"type": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5:2487054984108217856",
|
||||||
|
"name": "isFree",
|
||||||
|
"type": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6:5106135603734636032",
|
||||||
|
"name": "isPreview",
|
||||||
|
"type": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "7:8116657363890041600",
|
||||||
|
"name": "endPosition",
|
||||||
|
"type": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8:8837430652093702400",
|
||||||
|
"name": "playDateTime",
|
||||||
|
"type": 9
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"relations": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastEntityId": "1:1902306876074642688",
|
||||||
|
"lastIndexId": "0:0",
|
||||||
|
"lastRelationId": "0:0",
|
||||||
|
"lastSequenceId": "0:0",
|
||||||
|
"modelVersion": 5,
|
||||||
|
"modelVersionParserMinimum": 4,
|
||||||
|
"retiredEntityUids": [],
|
||||||
|
"retiredIndexUids": [],
|
||||||
|
"retiredPropertyUids": [],
|
||||||
|
"retiredRelationUids": [],
|
||||||
|
"version": 1
|
||||||
|
}
|
Loading…
Reference in New Issue