From 634f50d4f22d8c5c36c92331940b1ff4b430bb78 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Mon, 14 Aug 2023 19:22:23 +0900 Subject: [PATCH] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../btn_big_share.imageset/Contents.json | 21 + .../btn_big_share.imageset/btn_big_share.png | Bin 0 -> 3017 bytes .../ic_avatar.imageset/Contents.json | 21 + .../ic_avatar.imageset/ic_avatar.png | Bin 0 -> 362 bytes .../ic_mic_colored.imageset/Contents.json | 21 + .../ic_mic_colored.png | Bin 0 -> 1111 bytes SodaLive/Sources/App/AppStep.swift | 19 + SodaLive/Sources/ContentView.swift | 23 + .../Dialog/LiveRoomPasswordDialog.swift | 10 +- .../Profile/UserProfileViewModel.swift | 32 +- .../Live/Cancel/CancelLiveRequest.swift | 13 + .../Live/Cancel/LiveCancelDialog.swift | 77 +++ SodaLive/Sources/Live/LiveApi.swift | 16 +- SodaLive/Sources/Live/LiveRepository.swift | 8 + SodaLive/Sources/Live/LiveView.swift | 9 +- SodaLive/Sources/Live/LiveViewModel.swift | 225 +++++++- .../Live/Now/All/LiveAllViewModel.swift | 13 + .../Live/Now/All/LiveNowAllItemView.swift | 117 ++++ .../Sources/Live/Now/All/LiveNowAllView.swift | 83 +++ .../Sources/Live/Now/LiveNowItemView.swift | 111 ++++ .../Sources/Live/Now/SectionLiveNowView.swift | 82 +++ .../All/DateWithWeekDaySymbol.swift | 14 + .../All/LiveReservationAllItemView.swift | 91 ++++ .../All/LiveReservationAllView.swift | 125 +++++ .../Reservation/All/WeekCalendarView.swift | 95 ++++ .../MakeLiveReservationRequest.swift | 2 +- .../SectionLiveReservationView.swift | 37 +- .../Room/Detail/GetRoomDetailResponse.swift | 8 +- .../Live/Room/Detail/LiveDetailView.swift | 510 ++++++++++++++++++ .../Room/Detail/LiveDetailViewModel.swift | 162 ++++++ .../Room/Edit/EditLiveRoomInfoRequest.swift | 16 + .../Live/Room/Edit/LiveRoomEditView.swift | 292 ++++++++++ .../Room/Edit/LiveRoomEditViewModel.swift | 178 ++++++ .../Room/EnterOrQuitLiveRoomRequest.swift | 2 +- .../EntityInfo-SodaLive-dev.generated.swift | 259 +++++++++ generated/EntityInfo-SodaLive.generated.swift | 57 ++ model-SodaLive-dev.json | 67 +++ 37 files changed, 2767 insertions(+), 49 deletions(-) create mode 100644 SodaLive/Resources/Assets.xcassets/btn_big_share.imageset/Contents.json create mode 100644 SodaLive/Resources/Assets.xcassets/btn_big_share.imageset/btn_big_share.png create mode 100644 SodaLive/Resources/Assets.xcassets/ic_avatar.imageset/Contents.json create mode 100644 SodaLive/Resources/Assets.xcassets/ic_avatar.imageset/ic_avatar.png create mode 100644 SodaLive/Resources/Assets.xcassets/ic_mic_colored.imageset/Contents.json create mode 100644 SodaLive/Resources/Assets.xcassets/ic_mic_colored.imageset/ic_mic_colored.png create mode 100644 SodaLive/Sources/Live/Cancel/CancelLiveRequest.swift create mode 100644 SodaLive/Sources/Live/Cancel/LiveCancelDialog.swift create mode 100644 SodaLive/Sources/Live/Now/All/LiveAllViewModel.swift create mode 100644 SodaLive/Sources/Live/Now/All/LiveNowAllItemView.swift create mode 100644 SodaLive/Sources/Live/Now/All/LiveNowAllView.swift create mode 100644 SodaLive/Sources/Live/Now/LiveNowItemView.swift create mode 100644 SodaLive/Sources/Live/Reservation/All/DateWithWeekDaySymbol.swift create mode 100644 SodaLive/Sources/Live/Reservation/All/LiveReservationAllItemView.swift create mode 100644 SodaLive/Sources/Live/Reservation/All/LiveReservationAllView.swift create mode 100644 SodaLive/Sources/Live/Reservation/All/WeekCalendarView.swift create mode 100644 SodaLive/Sources/Live/Room/Detail/LiveDetailView.swift create mode 100644 SodaLive/Sources/Live/Room/Detail/LiveDetailViewModel.swift create mode 100644 SodaLive/Sources/Live/Room/Edit/EditLiveRoomInfoRequest.swift create mode 100644 SodaLive/Sources/Live/Room/Edit/LiveRoomEditView.swift create mode 100644 SodaLive/Sources/Live/Room/Edit/LiveRoomEditViewModel.swift create mode 100644 generated/EntityInfo-SodaLive-dev.generated.swift create mode 100644 generated/EntityInfo-SodaLive.generated.swift create mode 100644 model-SodaLive-dev.json diff --git a/SodaLive/Resources/Assets.xcassets/btn_big_share.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/btn_big_share.imageset/Contents.json new file mode 100644 index 0000000..6a7e26c --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/btn_big_share.imageset/Contents.json @@ -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 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/btn_big_share.imageset/btn_big_share.png b/SodaLive/Resources/Assets.xcassets/btn_big_share.imageset/btn_big_share.png new file mode 100644 index 0000000000000000000000000000000000000000..7eb49e536692fa126d28b099dfddf7431d14808e GIT binary patch literal 3017 zcmZ{mXH*l)630OZB@rVKq)HR%girzsQVj_KX#r_cB3&tJ5Ghil^ddzdG|_7SBLspJ zK|u(hln|QqdMTn(1VO5BdGWn(@4WY6cJ}|9J+o)_oH;v*mgXofpb(IWiHQqsjKDBF z;g4~!F;;;#aS4XwyldIRn8y~EWcC|#F! z$WxyX-dKHHDXE?9rz~XaFBE%SLr|k`I7cW-mWQ{SYOBLRvlrJt1$l`3CwEU89MT@X zIon$he4>}w1ZF|2>lFw7i1<3QYW#Rw07@J)Vgnv#^IU%QHf-k#xicjl30wS@8_5Pz zHu|yRty`jx|Thsu&-X52z|BOzJn@X`gO5x>O8zo6qkkFy|w`UrZ4}?~xv( znx@olcXkCBl{>(QhV*&O4szKRC@b@!7W9dYqRB|p{ovCku44nzgX$431H*aQLa!WQ zZ|}BLl(jdwIMPiuf{ncRWn!h`nFf(J5S2~>LFZoY!$&wvtrT8bt!nGg#Z7cVRHeaN z?kV=1r5hv3@{>jzJcDcZ;VdypfI#a7yRdxZ@Zs)n3lA4x#mw-@-JqVo3&xs!;^fk5 zvP*lDSbwjgV)tAjOVWLTj;(W%A=y?mRoE6nUHjgz!p7Oft^++=7?Z35!L_m*HRieX zGe}~0!6Gl)xc((Jz!5<=YBc6<@dz->QQB#~2nYlnYKP^QI(aDQc3Wm0m)gu11K%Z* zI5cr3Di|UI-#xX{;N}#gW-!MuOVIAmrut;yGB8J`><>+SYcA0n`uyB)B>ft5c0Y;C z?dR%F`#W!0u&Aw7R_&GO)H&*Or(Wr+GAE)40n}XO3E7N-aIKTsdpk=(K^@%Nf0rkK|>L4VIA+A)|B^?@dq#M0)7Zf5QVmqvzATKRoYuO zHX?h1ahxcmGDg7#iv!|j-+QyR6JLvl2{v~H5b}y)-+7$rgw$4@3xvhp2i0ZCYiwQc zW|adV+a}n85-AecawH)Ek4f`-?v|&e_@x9jp6kte;r-HtNgqPBWb6v=T4Pnte5#n? z1JF{~vc_MPmxqeOz5_pfGiv)wpaO^!;$Bk<%v$f4l3KParE}leE)&^{_!LyHpOwBT zDKNWc!8g4plxyo9n+~&&vN~=1jV}=3{*{%J)0NUge4Nhl0xN})6-a~tk&?0a4C8-G zdjuA4aMBY~|HjVvmHzAC$Q|#RD;Ha7Y9Uu?wgObc<)w1|WL%8Ws;>h(0F?K<)~Ch; zQei$WF9;~VpkacfuB7;9T2a`rSqyA2F3G?wC6j~x>599&WCAys<-UW)ZO^seCt_+3 zSpnUw-TD|AcXy?6{k?>Vdyu8wV zewRYV-Yh}trI!a;xQppeN#|WR(zOLAwbPU|(4#gp5ZR&X7q^spl`UxN@x>-iloDxQ zd1_|8R|{8WwO9qx@`9W8LQXTg#z=C59Tz$`1%I7m@@`tQs%+7B-On3*UT<$_UVMr5 zjJ{S?2kufSQcZjV&>}G^;=%IvS!>Qdyd!UO&G1pAOP4)jT1 zbXE%fVEnM{lR37BH97o>YaX;wmHWfj__C3zhXqpIvIJUA1c^nZtv8kSWP75^Zpl2! z19Gj02B?Oy?}_W#j-{nY8GAKjzbtJ@2Mdp^d|u0RP-eW%7yU-@VZ@K5-iFl=nwefX zy(?+9;aCHJj%d=!V${Y*L+BG6xC^{d^@G!}nPLkgA~)`|h;_juHy+tT<&uOb!)|o; z-PpNV2-_$y4m~VBup@+OgvF|U3vh1YJ1R+7MGDyhM@hF-eBwmqWYdK4p}0DBqQP*| zT;vJbkN;jhhLYt>C8?y5^}j3$wP-?C&D1M+PuJOb>)#aXOtmj?hGL7b&hdwDXOb6@ z73g%3y(V~c=APG7Gj0%MEx7+2T)@_@!3$6b)xPEMk#26M=VP4Y)GALE|2Q9=_IV<9 zJywUf@?GF5^!(}J$?Gwk({sf~1>pz9!3wh=c{Gl>oE4Ox(lHz7@|YT*Y+JNg^wrQ8 z`gl0+Dof@=M_qu785En1jX$M(iKr5NE7|%^c$2%P(Nig_KiHqSW`p@FC|D!A@ z|8p#}ennzJF?v#*vDaN0Vev|KipDn%mz#A;H!=aTQtfD)ROHM#{{pAhL%5@yU9J7a z@nad+fG`hhy~Z>4a{Lr|qlK$Szl-*Wzq6?erqw$kH#nM448Wa?^pr|AUxfP$GLE@2 zYmUh;17u0j;w^QeQ_7)bax2F6IQZwvcPZE)$=)u$+H$CCQyqcgTlu^;#3$-W2RHDX z>cPj8fOqwz=U_bCzH4b&<6NQk>;bzP>EZAB>%L!3K@?Ak=gH)+!;CWj)9L@!`2Sbl zdK~~%pFGgSzUA{n%qgm8W4%IYz|n2VpD}Wo2q3_e72psxNH|ucao?aWGN+C^-KrkK7HR-4jNZ7POJ8c?W7lJ1RDXGlC@#^fe`Ny&NX)|Ym zpmfPHaXTmeZ5jI2uT))SOS6beD+DW|&x;dyTHY?Rgooz+;6fD?xkdW!FKNdb;iyYp z>gWftc2Okk%S%F3^{1XzYgeI26^sIhr9as#bA*U0goyDqpKXkFe-g;e);%QBl^K z9|vAN8qj#e?`&}&m%)t@9RH;&8y4fguBGL9kGYc<)z4RU#3={Bb88>~&$J`!IperX z&ZwLWEd)%_Y|th#JCkg4O7?lbz->U$!=S((Ajrd{;598(}8xqeZOb|`5Y^| z51%d?{R~JslkU#>lSO7)Yo=l049{DuH=aVgo`;XT>B^r)OuG}NMGP(W1l3IU zUzZZF>}IohQ}s(dm2c2=w!Sy?pWK;Pl#GgrVaZScP)mKK$zH1|@| zm!iFgSadG946ANC5t3x#j*3Pj_|OJ9-XI6IF$np<;OWZkjofExYDidao=MUHW>}FB zug%n;s5?k8ydMxSt+-^mz(v#|!bg4AS#Z}t`=hSUrU}dJdk6RFYmP9mojQ^3vN4;V zre0jNB-H|w+YamK3cMAR^XL&<48L177uT$~c2i5Ih(xZ1Jh81}v5TIA4|n$0IxVSA zy#hNs;Hp*Odc2U8+L14}*PP-lhy`C=tY7wY6yE n+jO=(H@VczrKK&wret4GuNpny=`*Urn7ods|49KZc5j$~dihI&{DK*N2%O#7W2f>e?16Osf_ViT9DzWo zy`C_QTf)!(93KjGcj+_#U)-e6b>p!ZU)bUk$CFRgyKRh>bCy#T{FvbN z-`rtR`Z1;doSyTTyZ@96J>i_`f1Hzcz NgQu&X%Q~loCIEYUr#=7x literal 0 HcmV?d00001 diff --git a/SodaLive/Resources/Assets.xcassets/ic_mic_colored.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_mic_colored.imageset/Contents.json new file mode 100644 index 0000000..706c074 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_mic_colored.imageset/Contents.json @@ -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 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_mic_colored.imageset/ic_mic_colored.png b/SodaLive/Resources/Assets.xcassets/ic_mic_colored.imageset/ic_mic_colored.png new file mode 100644 index 0000000000000000000000000000000000000000..c97a2b23268feb5190de43631d46d9e585bb0aeb GIT binary patch literal 1111 zcmV-d1gQIoP)of5>Zsnh$35 z>p}!WYz<;R(tOR>#^rUQ)n+6{rVEN{(jD?ImRbHr)V5uayTi)eA%(;0auE)1P*|3} zBcGd2<}Mg7yh~vb$n1B>{QlgjQDD$dZ<)^JF8C)=6NeFnrDi&jyWmHW7(jHL!i&~Y zNLtE4^up!g(4sF)Cvp*t+}qKWE76?-tB?m9s(ll=FdloIng5H@< z1|4Z7CkS}i$-_uAlh-|q=ftB%cq=vkK;0aBb1!t_cqP)?1UrT?ZzU!;{?t-RdL6 zp6qE-4+GJcWayVBr+qPZAMV9sS1KB}Y+}_%GDcq+JN$|QktJv?SM(8dU=lm|NMv92 zkhtm#1!Ar~j6TbQi*7||Q^aS>133-Vkv}=2Ufpv;iPfboiktA^ZX}*i4y5hFD-ip}c7fX4 z?eMjT#{5C+7O_13ZRRDGadw#dt>lepeZ}X2`|D=tCY;2{VUTF=vgaAgqFWar)uG1qZ3jOD~~QjtjHWl4)>}3YBuUifF<~jDnBsg9}%# zQqE3}QFs=%pw)0$xv;<~r?tC4Vi}{u8MieHNjWaq9R5V 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 + ) } diff --git a/SodaLive/Sources/ContentView.swift b/SodaLive/Sources/ContentView.swift index 5b6b544..5480aae 100644 --- a/SodaLive/Sources/ContentView.swift +++ b/SodaLive/Sources/ContentView.swift @@ -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) diff --git a/SodaLive/Sources/Dialog/LiveRoomPasswordDialog.swift b/SodaLive/Sources/Dialog/LiveRoomPasswordDialog.swift index 231131f..4348db4 100644 --- a/SodaLive/Sources/Dialog/LiveRoomPasswordDialog.swift +++ b/SodaLive/Sources/Dialog/LiveRoomPasswordDialog.swift @@ -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 } diff --git a/SodaLive/Sources/Explorer/Profile/UserProfileViewModel.swift b/SodaLive/Sources/Explorer/Profile/UserProfileViewModel.swift index 8bf5ba7..228d39c 100644 --- a/SodaLive/Sources/Explorer/Profile/UserProfileViewModel.swift +++ b/SodaLive/Sources/Explorer/Profile/UserProfileViewModel.swift @@ -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) diff --git a/SodaLive/Sources/Live/Cancel/CancelLiveRequest.swift b/SodaLive/Sources/Live/Cancel/CancelLiveRequest.swift new file mode 100644 index 0000000..3fdaa0a --- /dev/null +++ b/SodaLive/Sources/Live/Cancel/CancelLiveRequest.swift @@ -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 +} diff --git a/SodaLive/Sources/Live/Cancel/LiveCancelDialog.swift b/SodaLive/Sources/Live/Cancel/LiveCancelDialog.swift new file mode 100644 index 0000000..2b71c54 --- /dev/null +++ b/SodaLive/Sources/Live/Cancel/LiveCancelDialog.swift @@ -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 + } + } +} diff --git a/SodaLive/Sources/Live/LiveApi.swift b/SodaLive/Sources/Live/LiveApi.swift index 9f02d71..d5c35ff 100644 --- a/SodaLive/Sources/Live/LiveApi.swift +++ b/SodaLive/Sources/Live/LiveApi.swift @@ -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) } } diff --git a/SodaLive/Sources/Live/LiveRepository.swift b/SodaLive/Sources/Live/LiveRepository.swift index c5926a3..98bda99 100644 --- a/SodaLive/Sources/Live/LiveRepository.swift +++ b/SodaLive/Sources/Live/LiveRepository.swift @@ -56,4 +56,12 @@ final class LiveRepository { func startLive(roomId: Int) -> AnyPublisher { return api.requestPublisher(.startLive(request: StartLiveRequest(roomId: roomId))) } + + func cancelRoom(roomId: Int, reason: String) -> AnyPublisher { + return api.requestPublisher(.cancelRoom(request: CancelLiveRequest(roomId: roomId, reason: reason))) + } + + func editLiveRoomInfo(roomId: Int, parameters: [MultipartFormData]) -> AnyPublisher { + return api.requestPublisher(.editLiveRoomInfo(roomId: roomId, parameters: parameters)) + } } diff --git a/SodaLive/Sources/Live/LiveView.swift b/SodaLive/Sources/Live/LiveView.swift index c45e753..d22e003 100644 --- a/SodaLive/Sources/Live/LiveView.swift +++ b/SodaLive/Sources/Live/LiveView.swift @@ -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) } } } diff --git a/SodaLive/Sources/Live/LiveViewModel.swift b/SodaLive/Sources/Live/LiveViewModel.swift index c2a90e8..6258e9b 100644 --- a/SodaLive/Sources/Live/LiveViewModel.swift +++ b/SodaLive/Sources/Live/LiveViewModel.swift @@ -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.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.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 diff --git a/SodaLive/Sources/Live/Now/All/LiveAllViewModel.swift b/SodaLive/Sources/Live/Now/All/LiveAllViewModel.swift new file mode 100644 index 0000000..5435805 --- /dev/null +++ b/SodaLive/Sources/Live/Now/All/LiveAllViewModel.swift @@ -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 +} diff --git a/SodaLive/Sources/Live/Now/All/LiveNowAllItemView.swift b/SodaLive/Sources/Live/Now/All/LiveNowAllItemView.swift new file mode 100644 index 0000000..e66b205 --- /dev/null +++ b/SodaLive/Sources/Live/Now/All/LiveNowAllItemView.swift @@ -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) + } +} diff --git a/SodaLive/Sources/Live/Now/All/LiveNowAllView.swift b/SodaLive/Sources/Live/Now/All/LiveNowAllView.swift new file mode 100644 index 0000000..dfd7c90 --- /dev/null +++ b/SodaLive/Sources/Live/Now/All/LiveNowAllView.swift @@ -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.. 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() + } + } +} diff --git a/SodaLive/Sources/Live/Now/LiveNowItemView.swift b/SodaLive/Sources/Live/Now/LiveNowItemView.swift new file mode 100644 index 0000000..e2f5123 --- /dev/null +++ b/SodaLive/Sources/Live/Now/LiveNowItemView.swift @@ -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 + ) + ) + } +} diff --git a/SodaLive/Sources/Live/Now/SectionLiveNowView.swift b/SodaLive/Sources/Live/Now/SectionLiveNowView.swift index b5ad05b..505949a 100644 --- a/SodaLive/Sources/Live/Now/SectionLiveNowView.swift +++ b/SodaLive/Sources/Live/Now/SectionLiveNowView.swift @@ -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) + } } } } diff --git a/SodaLive/Sources/Live/Reservation/All/DateWithWeekDaySymbol.swift b/SodaLive/Sources/Live/Reservation/All/DateWithWeekDaySymbol.swift new file mode 100644 index 0000000..d13d5ba --- /dev/null +++ b/SodaLive/Sources/Live/Reservation/All/DateWithWeekDaySymbol.swift @@ -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 +} diff --git a/SodaLive/Sources/Live/Reservation/All/LiveReservationAllItemView.swift b/SodaLive/Sources/Live/Reservation/All/LiveReservationAllItemView.swift new file mode 100644 index 0000000..29e5216 --- /dev/null +++ b/SodaLive/Sources/Live/Reservation/All/LiveReservationAllItemView.swift @@ -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) + } +} diff --git a/SodaLive/Sources/Live/Reservation/All/LiveReservationAllView.swift b/SodaLive/Sources/Live/Reservation/All/LiveReservationAllView.swift new file mode 100644 index 0000000..ec78601 --- /dev/null +++ b/SodaLive/Sources/Live/Reservation/All/LiveReservationAllView.swift @@ -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.. 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 + } + } + ) + } + } + } +} diff --git a/SodaLive/Sources/Live/Reservation/All/WeekCalendarView.swift b/SodaLive/Sources/Live/Reservation/All/WeekCalendarView.swift new file mode 100644 index 0000000..b8a73db --- /dev/null +++ b/SodaLive/Sources/Live/Reservation/All/WeekCalendarView.swift @@ -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 + ) + } +} diff --git a/SodaLive/Sources/Live/Reservation/MakeLiveReservationRequest.swift b/SodaLive/Sources/Live/Reservation/MakeLiveReservationRequest.swift index 21dc70e..6718c7b 100644 --- a/SodaLive/Sources/Live/Reservation/MakeLiveReservationRequest.swift +++ b/SodaLive/Sources/Live/Reservation/MakeLiveReservationRequest.swift @@ -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 } diff --git a/SodaLive/Sources/Live/Reservation/SectionLiveReservationView.swift b/SodaLive/Sources/Live/Reservation/SectionLiveReservationView.swift index f20b83b..e51d460 100644 --- a/SodaLive/Sources/Live/Reservation/SectionLiveReservationView.swift +++ b/SodaLive/Sources/Live/Reservation/SectionLiveReservationView.swift @@ -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") diff --git a/SodaLive/Sources/Live/Room/Detail/GetRoomDetailResponse.swift b/SodaLive/Sources/Live/Room/Detail/GetRoomDetailResponse.swift index ff631bc..eb9bc5c 100644 --- a/SodaLive/Sources/Live/Room/Detail/GetRoomDetailResponse.swift +++ b/SodaLive/Sources/Live/Room/Detail/GetRoomDetailResponse.swift @@ -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 { diff --git a/SodaLive/Sources/Live/Room/Detail/LiveDetailView.swift b/SodaLive/Sources/Live/Room/Detail/LiveDetailView.swift new file mode 100644 index 0000000..d526aca --- /dev/null +++ b/SodaLive/Sources/Live/Room/Detail/LiveDetailView.swift @@ -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.. 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() + } + } +} diff --git a/SodaLive/Sources/Live/Room/Detail/LiveDetailViewModel.swift b/SodaLive/Sources/Live/Room/Detail/LiveDetailViewModel.swift new file mode 100644 index 0000000..a4b0a1d --- /dev/null +++ b/SodaLive/Sources/Live/Room/Detail/LiveDetailViewModel.swift @@ -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() + + @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.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 + } + } +} diff --git a/SodaLive/Sources/Live/Room/Edit/EditLiveRoomInfoRequest.swift b/SodaLive/Sources/Live/Room/Edit/EditLiveRoomInfoRequest.swift new file mode 100644 index 0000000..b777cb4 --- /dev/null +++ b/SodaLive/Sources/Live/Room/Edit/EditLiveRoomInfoRequest.swift @@ -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? +} diff --git a/SodaLive/Sources/Live/Room/Edit/LiveRoomEditView.swift b/SodaLive/Sources/Live/Room/Edit/LiveRoomEditView.swift new file mode 100644 index 0000000..d9f2267 --- /dev/null +++ b/SodaLive/Sources/Live/Room/Edit/LiveRoomEditView.swift @@ -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) + } + } +} diff --git a/SodaLive/Sources/Live/Room/Edit/LiveRoomEditViewModel.swift b/SodaLive/Sources/Live/Room/Edit/LiveRoomEditViewModel.swift new file mode 100644 index 0000000..f92f7ff --- /dev/null +++ b/SodaLive/Sources/Live/Room/Edit/LiveRoomEditViewModel.swift @@ -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() + + 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 + } +} diff --git a/SodaLive/Sources/Live/Room/EnterOrQuitLiveRoomRequest.swift b/SodaLive/Sources/Live/Room/EnterOrQuitLiveRoomRequest.swift index f6624fe..7338ddc 100644 --- a/SodaLive/Sources/Live/Room/EnterOrQuitLiveRoomRequest.swift +++ b/SodaLive/Sources/Live/Room/EnterOrQuitLiveRoomRequest.swift @@ -10,5 +10,5 @@ import Foundation struct EnterOrQuitLiveRoomRequest: Encodable { let roomId: Int let container: String = "ios" - var password: Int? = nil + var password: String? = nil } diff --git a/generated/EntityInfo-SodaLive-dev.generated.swift b/generated/EntityInfo-SodaLive-dev.generated.swift new file mode 100644 index 0000000..de5dfb1 --- /dev/null +++ b/generated/EntityInfo-SodaLive-dev.generated.swift @@ -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 { + return EntityId(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 { return Property(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 { return Property(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 { return Property(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 { return Property(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 { return Property(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 { return Property(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 { return Property(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 { return Property(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 { return Property(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 { return Property(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 { return Property(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 { return Property(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 { return Property(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 { return Property(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 { return Property(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 { return Property(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(_ 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 diff --git a/generated/EntityInfo-SodaLive.generated.swift b/generated/EntityInfo-SodaLive.generated.swift new file mode 100644 index 0000000..ccabd90 --- /dev/null +++ b/generated/EntityInfo-SodaLive.generated.swift @@ -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(_ 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 diff --git a/model-SodaLive-dev.json b/model-SodaLive-dev.json new file mode 100644 index 0000000..df8180f --- /dev/null +++ b/model-SodaLive-dev.json @@ -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 +} \ No newline at end of file