라이브 상세 페이지 추가
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,
|
||||
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
|
||||
)
|
||||
|
||||
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:
|
||||
EmptyView()
|
||||
.frame(width: 0, height: 0, alignment: .topLeading)
|
||||
|
|
|
@ -12,7 +12,7 @@ struct LiveRoomPasswordDialog: View {
|
|||
@Binding var isShowing: Bool
|
||||
|
||||
let can: Int
|
||||
let confirmAction: (Int) -> Void
|
||||
let confirmAction: (String) -> Void
|
||||
|
||||
@State private var password = ""
|
||||
@StateObject var keyboardHandler = KeyboardHandler()
|
||||
|
@ -81,9 +81,9 @@ struct LiveRoomPasswordDialog: View {
|
|||
.cornerRadius(8)
|
||||
.onTapGesture {
|
||||
if password.trimmingCharacters(in: .whitespaces).isEmpty {
|
||||
confirmAction(0)
|
||||
confirmAction("")
|
||||
} else {
|
||||
confirmAction(Int(password)!)
|
||||
confirmAction(password)
|
||||
}
|
||||
isShowing = false
|
||||
}
|
||||
|
@ -97,9 +97,9 @@ struct LiveRoomPasswordDialog: View {
|
|||
.cornerRadius(8)
|
||||
.onTapGesture {
|
||||
if password.trimmingCharacters(in: .whitespaces).isEmpty {
|
||||
confirmAction(0)
|
||||
confirmAction("")
|
||||
} else {
|
||||
confirmAction(Int(password)!)
|
||||
confirmAction(password)
|
||||
}
|
||||
isShowing = false
|
||||
}
|
||||
|
|
|
@ -30,13 +30,9 @@ final class UserProfileViewModel: ObservableObject {
|
|||
@Published var paymentDialogConfirmAction = {}
|
||||
@Published var paymentDialogConfirmTitle = ""
|
||||
|
||||
@Published var secretDialogManagerNickname = ""
|
||||
@Published var secretDialogConfirmAction = {}
|
||||
@Published var isShowSecretDialog = false
|
||||
|
||||
@Published var secretOrPasswordDialogCan = 0
|
||||
|
||||
@Published var passwordDialogConfirmAction: (Int) -> Void = { _ in }
|
||||
@Published var passwordDialogConfirmAction: (String) -> Void = { _ in }
|
||||
@Published var isShowPasswordDialog = false
|
||||
|
||||
@Published var navigationTitle = "채널"
|
||||
|
@ -101,7 +97,6 @@ final class UserProfileViewModel: ObservableObject {
|
|||
|
||||
func hidePaymentPopup() {
|
||||
isShowPaymentDialog = false
|
||||
isShowSecretDialog = false
|
||||
isShowPasswordDialog = false
|
||||
|
||||
paymentDialogTitle = ""
|
||||
|
@ -109,9 +104,6 @@ final class UserProfileViewModel: ObservableObject {
|
|||
paymentDialogConfirmAction = {}
|
||||
|
||||
secretOrPasswordDialogCan = 0
|
||||
secretDialogManagerNickname = ""
|
||||
secretDialogConfirmAction = {}
|
||||
|
||||
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
|
||||
let request = MakeLiveReservationRequest(roomId: roomId, password: password)
|
||||
liveRepository.makeReservation(request: request)
|
||||
|
@ -190,14 +182,7 @@ final class UserProfileViewModel: ObservableObject {
|
|||
self.enterRoom(roomId: roomId)
|
||||
}
|
||||
} else if ($0.price == 0 || $0.isPaid) {
|
||||
if $0.isSecretRoom {
|
||||
self.secretDialogManagerNickname = $0.manager.nickname
|
||||
self.secretDialogConfirmAction = {
|
||||
self.enterRoom(roomId: roomId)
|
||||
}
|
||||
self.secretOrPasswordDialogCan = 0
|
||||
self.isShowSecretDialog = true
|
||||
} else if $0.isPrivateRoom {
|
||||
if $0.isPrivateRoom {
|
||||
self.passwordDialogConfirmAction = { password in
|
||||
self.enterRoom(roomId: roomId, password: password)
|
||||
}
|
||||
|
@ -208,14 +193,7 @@ final class UserProfileViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
if $0.isSecretRoom {
|
||||
self.secretDialogManagerNickname = $0.manager.nickname
|
||||
self.secretDialogConfirmAction = {
|
||||
self.enterRoom(roomId: roomId)
|
||||
}
|
||||
self.secretOrPasswordDialogCan = $0.price
|
||||
self.isShowSecretDialog = true
|
||||
} else if $0.isPrivateRoom {
|
||||
if $0.isPrivateRoom {
|
||||
self.secretOrPasswordDialogCan = $0.price
|
||||
self.passwordDialogConfirmAction = { password in
|
||||
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
|
||||
let request = EnterOrQuitLiveRoomRequest(roomId: roomId, password: password)
|
||||
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 createRoom(parameters: [MultipartFormData])
|
||||
case startLive(request: StartLiveRequest)
|
||||
case cancelRoom(request: CancelLiveRequest)
|
||||
case editLiveRoomInfo(roomId: Int, parameters: [MultipartFormData])
|
||||
}
|
||||
|
||||
extension LiveApi: TargetType {
|
||||
|
@ -65,6 +67,12 @@ extension LiveApi: TargetType {
|
|||
|
||||
case .startLive:
|
||||
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:
|
||||
return .post
|
||||
|
||||
case .cancelReservation, .startLive:
|
||||
case .cancelReservation, .startLive, .cancelRoom, .editLiveRoomInfo:
|
||||
return .put
|
||||
}
|
||||
}
|
||||
|
@ -139,6 +147,12 @@ extension LiveApi: TargetType {
|
|||
|
||||
case .startLive(let 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> {
|
||||
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,
|
||||
alignment: .center
|
||||
)
|
||||
.padding(.vertical, 40)
|
||||
.padding(.top, 40)
|
||||
}
|
||||
|
||||
if viewModel.liveReservationItems.count > 0 {
|
||||
SectionLiveReservationView(
|
||||
items: viewModel.liveReservationItems,
|
||||
onClickCancel: { viewModel.getSummary() },
|
||||
onClickStart: {_ in},
|
||||
onClickReservation: {_ in},
|
||||
onClickStart: { roomId in processStart(roomId: roomId) },
|
||||
onClickReservation: { roomId in
|
||||
viewModel.reservationLiveRoom(roomId: roomId)
|
||||
},
|
||||
onTapCreateLive: {
|
||||
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 secretOrPasswordDialogCoin = 0
|
||||
@Published var passwordDialogConfirmAction: (Int) -> Void = { _ in }
|
||||
@Published var passwordDialogConfirmAction: (String) -> Void = { _ in }
|
||||
@Published var isShowPasswordDialog = false
|
||||
|
||||
@Published var isFollowingList = UserDefaults.bool(forKey: .isFollowedChannel) {
|
||||
|
@ -52,6 +52,17 @@ final class LiveViewModel: ObservableObject {
|
|||
var isLast = false
|
||||
private let pageSize = 10
|
||||
|
||||
var selectedDateString: String = "" {
|
||||
didSet {
|
||||
if !selectedDateString.trimmingCharacters(in: .whitespaces).isEmpty {
|
||||
page = 1
|
||||
isLast = false
|
||||
liveReservationItems.removeAll()
|
||||
getLiveReservationList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hidePopup() {
|
||||
isShowPaymentDialog = false
|
||||
isShowPasswordDialog = false
|
||||
|
@ -176,7 +187,7 @@ final class LiveViewModel: ObservableObject {
|
|||
.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
|
||||
let request = EnterOrQuitLiveRoomRequest(roomId: roomId, password: password)
|
||||
repository.enterRoom(request: request)
|
||||
|
@ -260,6 +271,216 @@ final class LiveViewModel: ObservableObject {
|
|||
.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() {
|
||||
followedChannelItems.removeAll()
|
||||
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 {
|
||||
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 {
|
||||
let roomId: Int
|
||||
let password: Int?
|
||||
let password: String?
|
||||
let container: String = "ios"
|
||||
let timezone: String = TimeZone.current.identifier
|
||||
}
|
||||
|
|
|
@ -33,7 +33,16 @@ struct SectionLiveReservationView: View {
|
|||
Text("전체보기")
|
||||
.font(.custom(Font.light.rawValue, size: 11.3))
|
||||
.foregroundColor(Color(hex: "bbbbbb"))
|
||||
.onTapGesture {}
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(
|
||||
step: .liveReservationAll(
|
||||
onClickReservation: onClickReservation,
|
||||
onClickStart: onClickStart,
|
||||
onClickCancel: onClickCancel,
|
||||
onTapCreateLive: onTapCreateLive
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
@ -47,17 +56,37 @@ struct SectionLiveReservationView: View {
|
|||
if item.managerId == UserDefaults.int(forKey: .userId) {
|
||||
MyLiveReservationItemView(item: item, index: index)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {}
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(
|
||||
step: .liveDetail(
|
||||
roomId: item.roomId,
|
||||
onClickParticipant: {},
|
||||
onClickReservation: {},
|
||||
onClickStart: { onClickStart(item.roomId) },
|
||||
onClickCancel: onClickCancel
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LiveReservationItemView(item: item)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {}
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(
|
||||
step: .liveDetail(
|
||||
roomId: item.roomId,
|
||||
onClickParticipant: {},
|
||||
onClickReservation: {},
|
||||
onClickStart: { onClickStart(item.roomId) },
|
||||
onClickCancel: onClickCancel
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
.frame(width: screenSize().width)
|
||||
.padding(.top, 28.3)
|
||||
.padding(.top, 13.3)
|
||||
} else {
|
||||
VStack(spacing: 0) {
|
||||
Image("ic_no_item")
|
||||
|
|
|
@ -11,15 +11,13 @@ struct GetRoomDetailResponse: Decodable {
|
|||
let roomId: Int
|
||||
let price: Int
|
||||
let title: String
|
||||
let content: String
|
||||
let notice: String
|
||||
let isPaid: Bool
|
||||
let isPrivateRoom: Bool
|
||||
let isSecretRoom: Bool
|
||||
let password: Int?
|
||||
let password: String?
|
||||
let tags: [String]
|
||||
let channelName: String?
|
||||
let beginDateTime: String
|
||||
let isNotification: Bool
|
||||
let numberOfParticipants: Int
|
||||
let numberOfParticipantsTotal: Int
|
||||
let manager: GetRoomDetailManager
|
||||
|
@ -35,7 +33,7 @@ struct GetRoomDetailManager: Decodable {
|
|||
let websiteUrl: String?
|
||||
let blogUrl: String?
|
||||
let profileImageUrl: String
|
||||
let isCounselor: Bool
|
||||
let isCreator: Bool
|
||||
}
|
||||
|
||||
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 {
|
||||
let roomId: Int
|
||||
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