diff --git a/SodaLive/Resources/Assets.xcassets/btn_item_more.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/btn_item_more.imageset/Contents.json new file mode 100644 index 0000000..ca49f65 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/btn_item_more.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "btn_item_more.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/btn_item_more.imageset/btn_item_more.png b/SodaLive/Resources/Assets.xcassets/btn_item_more.imageset/btn_item_more.png new file mode 100644 index 0000000..f219986 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/btn_item_more.imageset/btn_item_more.png differ diff --git a/SodaLive/Resources/Assets.xcassets/btn_make_live.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/btn_make_live.imageset/Contents.json new file mode 100644 index 0000000..0190159 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/btn_make_live.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "btn_make_live.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/btn_make_live.imageset/btn_make_live.png b/SodaLive/Resources/Assets.xcassets/btn_make_live.imageset/btn_make_live.png new file mode 100644 index 0000000..8c1d45f Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/btn_make_live.imageset/btn_make_live.png differ diff --git a/SodaLive/Resources/Assets.xcassets/btn_toggle_off_big.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/btn_toggle_off_big.imageset/Contents.json new file mode 100644 index 0000000..c146706 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/btn_toggle_off_big.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "btn_toggle_off_big.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/btn_toggle_off_big.imageset/btn_toggle_off_big.png b/SodaLive/Resources/Assets.xcassets/btn_toggle_off_big.imageset/btn_toggle_off_big.png new file mode 100644 index 0000000..ee6af1d Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/btn_toggle_off_big.imageset/btn_toggle_off_big.png differ diff --git a/SodaLive/Resources/Assets.xcassets/btn_toggle_on_big.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/btn_toggle_on_big.imageset/Contents.json new file mode 100644 index 0000000..0382814 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/btn_toggle_on_big.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "btn_toggle_on_big.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/btn_toggle_on_big.imageset/btn_toggle_on_big.png b/SodaLive/Resources/Assets.xcassets/btn_toggle_on_big.imageset/btn_toggle_on_big.png new file mode 100644 index 0000000..6898f8d Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/btn_toggle_on_big.imageset/btn_toggle_on_big.png differ diff --git a/SodaLive/Resources/Assets.xcassets/ic_can.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_can.imageset/Contents.json new file mode 100644 index 0000000..5ba6682 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_can.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_can.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_can.imageset/ic_can.png b/SodaLive/Resources/Assets.xcassets/ic_can.imageset/ic_can.png new file mode 100644 index 0000000..ebd2064 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_can.imageset/ic_can.png differ diff --git a/SodaLive/Resources/Assets.xcassets/ic_lock.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_lock.imageset/Contents.json new file mode 100644 index 0000000..713e0ff --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_lock.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_lock.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_lock.imageset/ic_lock.png b/SodaLive/Resources/Assets.xcassets/ic_lock.imageset/ic_lock.png new file mode 100644 index 0000000..39e287d Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_lock.imageset/ic_lock.png differ diff --git a/SodaLive/Resources/Assets.xcassets/ic_no_item.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_no_item.imageset/Contents.json new file mode 100644 index 0000000..5d8b700 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_no_item.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_no_item.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_no_item.imageset/ic_no_item.png b/SodaLive/Resources/Assets.xcassets/ic_no_item.imageset/ic_no_item.png new file mode 100644 index 0000000..498f40c Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_no_item.imageset/ic_no_item.png differ diff --git a/SodaLive/Resources/Assets.xcassets/ic_plus_no_bg.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_plus_no_bg.imageset/Contents.json new file mode 100644 index 0000000..28f5c34 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_plus_no_bg.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_plus_no_bg.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_plus_no_bg.imageset/ic_plus_no_bg.png b/SodaLive/Resources/Assets.xcassets/ic_plus_no_bg.imageset/ic_plus_no_bg.png new file mode 100644 index 0000000..acb5e05 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_plus_no_bg.imageset/ic_plus_no_bg.png differ diff --git a/SodaLive/Resources/Assets.xcassets/img_how_to_use.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/img_how_to_use.imageset/Contents.json new file mode 100644 index 0000000..1d0b67a --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/img_how_to_use.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "img_how_to_use.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/img_how_to_use.imageset/img_how_to_use.png b/SodaLive/Resources/Assets.xcassets/img_how_to_use.imageset/img_how_to_use.png new file mode 100644 index 0000000..1f8cfa8 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/img_how_to_use.imageset/img_how_to_use.png differ diff --git a/SodaLive/Sources/Dialog/LiveRoomPasswordDialog.swift b/SodaLive/Sources/Dialog/LiveRoomPasswordDialog.swift new file mode 100644 index 0000000..231131f --- /dev/null +++ b/SodaLive/Sources/Dialog/LiveRoomPasswordDialog.swift @@ -0,0 +1,128 @@ +// +// LiveRoomPasswordDialog.swift +// SodaLive +// +// Created by klaus on 2023/08/10. +// + +import SwiftUI + +struct LiveRoomPasswordDialog: View { + + @Binding var isShowing: Bool + + let can: Int + let confirmAction: (Int) -> Void + + @State private var password = "" + @StateObject var keyboardHandler = KeyboardHandler() + + var body: some View { + GeometryReader { geo in + ZStack { + Color.black + .opacity(0.5) + .frame(width: geo.size.width, height: geo.size.height) + + VStack(spacing: 0) { + Text("비밀번호 입력") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color(hex: "bbbbbb")) + .padding(.top, 40) + + Text("비공개 라이브의 입장 비밀번호를\n입력해 주세요.") + .font(.custom(Font.medium.rawValue, size: 13)) + .foregroundColor(Color(hex: "bbbbbb")) + .multilineTextAlignment(.center) + .padding(.top, 12) + .padding(.horizontal, 13.3) + + UserTextField( + title: "비밀번호", + hint: "비밀번호를 입력해 주세요", + isSecure: false, + variable: $password, + keyboardType: .numberPad + ) + .padding(.horizontal, 13.3) + .padding(.top, 13.3) + + HStack(spacing: 13.3) { + Text("취소") + .font(.custom(Font.bold.rawValue, size: 15.3)) + .foregroundColor(Color(hex: "9970ff")) + .padding(.vertical, 16) + .frame(width: (geo.size.width - 66.7) / 3) + .background(Color(hex: "9970ff").opacity(0.13)) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(hex: "9970ff"), lineWidth: 1) + ) + .onTapGesture { + isShowing = false + } + + if can > 0 { + HStack(spacing: 0) { + Text("\(can)") + .font(.custom(Font.bold.rawValue, size: 15.3)) + .foregroundColor(Color(hex: "ffffff")) + + Image("ic_can") + + Text("으로 입장") + .font(.custom(Font.bold.rawValue, size: 15.3)) + .foregroundColor(Color(hex: "ffffff")) + } + .padding(.vertical, 16) + .frame(width: (geo.size.width - 66.7) * 2 / 3) + .background(Color(hex: "9970ff")) + .cornerRadius(8) + .onTapGesture { + if password.trimmingCharacters(in: .whitespaces).isEmpty { + confirmAction(0) + } else { + confirmAction(Int(password)!) + } + isShowing = false + } + } else { + Text("입장하기") + .font(.custom(Font.bold.rawValue, size: 15.3)) + .foregroundColor(Color(hex: "ffffff")) + .padding(.vertical, 16) + .frame(width: (geo.size.width - 66.7) * 2 / 3) + .background(Color(hex: "9970ff")) + .cornerRadius(8) + .onTapGesture { + if password.trimmingCharacters(in: .whitespaces).isEmpty { + confirmAction(0) + } else { + confirmAction(Int(password)!) + } + isShowing = false + } + } + } + .padding(.top, 45) + .padding(.bottom, 16.7) + } + .frame(width: geo.size.width - 26.7, alignment: .center) + .background(Color(hex: "222222")) + .cornerRadius(10) + .offset(y: 0 - (keyboardHandler.keyboardHeight / 3)) + } + } + } +} + +struct LiveRoomPasswordDialog_Previews: PreviewProvider { + static var previews: some View { + LiveRoomPasswordDialog( + isShowing: .constant(true), + can: 10, + confirmAction: { _ in } + ) + } +} diff --git a/SodaLive/Sources/Dialog/SodaDialog.swift b/SodaLive/Sources/Dialog/SodaDialog.swift new file mode 100644 index 0000000..7df2700 --- /dev/null +++ b/SodaLive/Sources/Dialog/SodaDialog.swift @@ -0,0 +1,107 @@ +// +// SodaDialog.swift +// SodaLive +// +// Created by klaus on 2023/08/10. +// + +import SwiftUI + +struct SodaDialog: View { + + let title: String + let desc: String + let confirmButtonTitle: String + let confirmButtonAction: () -> Void + let cancelButtonTitle: String + let cancelButtonAction: () -> Void + + init( + title: String, + desc: String, + confirmButtonTitle: String, + confirmButtonAction: @escaping () -> Void, + cancelButtonTitle: String = "", + cancelButtonAction: @escaping () -> Void = {} + ) { + self.title = title + self.desc = desc + self.confirmButtonTitle = confirmButtonTitle + self.confirmButtonAction = confirmButtonAction + self.cancelButtonTitle = cancelButtonTitle + self.cancelButtonAction = cancelButtonAction + } + + var body: some View { + GeometryReader { geo in + ZStack { + Color.black + .opacity(0.5) + .frame(width: geo.size.width, height: geo.size.height) + + VStack(spacing: 0) { + Text(title) + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color(hex: "bbbbbb")) + .padding(.top, 40) + + Text(desc) + .font(.custom(Font.medium.rawValue, size: 15)) + .foregroundColor(Color(hex: "bbbbbb")) + .multilineTextAlignment(.center) + .padding(.top, 12) + .padding(.horizontal, 13.3) + .fixedSize(horizontal: false, vertical: true) + + HStack(spacing: 13.3) { + if cancelButtonTitle.count > 0 { + Text(cancelButtonTitle) + .font(.custom(Font.bold.rawValue, size: 15.3)) + .foregroundColor(Color(hex: "9970ff")) + .padding(.vertical, 16) + .frame(width: (geo.size.width - 66.7) / 3) + .background(Color(hex: "9970ff").opacity(0.13)) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(hex: "9970ff"), lineWidth: 1) + ) + .onTapGesture { + cancelButtonAction() + } + } + + Text(confirmButtonTitle) + .font(.custom(Font.bold.rawValue, size: 15.3)) + .foregroundColor(Color(hex: "ffffff")) + .padding(.vertical, 16) + .frame(width: (geo.size.width - 66.7) * 2 / 3) + .background(Color(hex: "9970ff")) + .cornerRadius(8) + .onTapGesture { + confirmButtonAction() + } + } + .padding(.top, 45) + .padding(.bottom, 16.7) + } + .frame(width: geo.size.width - 26.7, alignment: .center) + .background(Color(hex: "222222")) + .cornerRadius(10) + } + } + } +} + +struct SodaDialog_Previews: PreviewProvider { + static var previews: some View { + SodaDialog( + title: "작성글 등록", + desc: "작성한 글을 등록하시겠습니까?", + confirmButtonTitle: "결제 후 입장", + confirmButtonAction: {}, + cancelButtonTitle: "취소", + cancelButtonAction: {} + ) + } +} diff --git a/SodaLive/Sources/Extensions/UserDefaultsExtension.swift b/SodaLive/Sources/Extensions/UserDefaultsExtension.swift index 4677040..7713f03 100644 --- a/SodaLive/Sources/Extensions/UserDefaultsExtension.swift +++ b/SodaLive/Sources/Extensions/UserDefaultsExtension.swift @@ -19,7 +19,7 @@ enum UserDefaultsKey: String, CaseIterable { case profileImage case devicePushToken case isContentPlayLoop - case isFollowedCreatorLive + case isFollowedChannel case isViewedOnboardingView case notShowingEventPopupId } diff --git a/SodaLive/Sources/Live/EventBanner/SectionEventBannerView.swift b/SodaLive/Sources/Live/EventBanner/SectionEventBannerView.swift new file mode 100644 index 0000000..5decacd --- /dev/null +++ b/SodaLive/Sources/Live/EventBanner/SectionEventBannerView.swift @@ -0,0 +1,93 @@ +// +// SectionEventBannerView.swift +// SodaLive +// +// Created by klaus on 2023/08/09. +// + +import SwiftUI +import Kingfisher + +struct SectionEventBannerView: View { + + @State private var currentIndex = -1 + @State private var timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect() + + let items: [EventItem] + + var body: some View { + GeometryReader { proxy in + VStack(spacing: 13.3) { + TabView(selection: $currentIndex) { + ForEach(0.. 0, let url = URL(string: link), UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + } + } else { + KFImage(URL(string: item.thumbnailImageUrl)) + .resizable() + .frame(width: proxy.size.width, height: proxy.size.height, alignment: .center) + .tag(index) + .onTapGesture { + if let _ = item.detailImageUrl { + } else if let link = item.link, link.trimmingCharacters(in: .whitespaces).count > 0, let url = URL(string: link), UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + } + } + } + } + .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) + .frame( + width: proxy.size.width, + height: proxy.size.height, + alignment: .center + ) + + HStack(spacing: 4) { + ForEach(0..() + + func roomList(request: GetRoomListRequest) -> AnyPublisher { + return api.requestPublisher(.roomList(request: request)) + } +} diff --git a/SodaLive/Sources/Live/LiveView.swift b/SodaLive/Sources/Live/LiveView.swift index 9494c66..e73b389 100644 --- a/SodaLive/Sources/Live/LiveView.swift +++ b/SodaLive/Sources/Live/LiveView.swift @@ -6,10 +6,123 @@ // import SwiftUI +import RefreshableScrollView struct LiveView: View { + + @StateObject var viewModel = LiveViewModel() + @StateObject var appState = AppState.shared + var body: some View { - Text("Live") + ZStack { + Color.black.ignoresSafeArea() + + GeometryReader { geo in + ZStack(alignment: .bottomTrailing) { + RefreshableScrollView( + refreshing: $viewModel.isRefresh, + action: { + viewModel.getSummary() + } + ) { + VStack(spacing: 0) { + if viewModel.recommendLiveItems.count > 0 { + SectionRecommendLiveView(items: viewModel.recommendLiveItems) + .padding(.top, 13.3) + } + + if let url = URL(string: "https://blog.naver.com/yozmlive"), + UIApplication.shared.canOpenURL(url) { + Image("img_how_to_use") + .resizable() + .frame( + width: screenSize().width, + height: (200 * screenSize().width) / 1080 + ) + .padding(.top, 21.3) + .onTapGesture { + UIApplication.shared.open(url) + } + } + + if viewModel.recommendChannelItems.count > 0 { + SectionRecommendChannelView( + items: viewModel.isFollowingList ? + viewModel.followedChannelItems : + viewModel.recommendChannelItems, + isFollowingList: $viewModel.isFollowingList + ) + .padding(.top, 40) + } + + if viewModel.liveNowItems.count > 0 { + SectionLiveNowView( + items: viewModel.liveNowItems, + onClickParticipant: {_ in}, + onTapCreateLive: {} + ) + .padding(.top, 40) + } + + if viewModel.eventBannerItems.count > 0 { + SectionEventBannerView(items: viewModel.eventBannerItems) + .frame( + width: viewModel.eventBannerItems.count > 0 ? screenSize().width : 0, + height: viewModel.eventBannerItems.count > 0 ? screenSize().width * 300 / 1000 : 0, + alignment: .center + ) + .padding(.vertical, 40) + } + + if viewModel.liveReservationItems.count > 0 { + SectionLiveReservationView( + items: viewModel.liveReservationItems, + onClickCancel: { viewModel.getSummary() }, + onClickStart: {_ in}, + onClickReservation: {_ in}, + onTapCreateLive: {} + ) + } + } + } + .frame(width: geo.size.width, height: geo.size.height) + .onAppear { + viewModel.getSummary() + } + + Image("btn_make_live") + .padding(.trailing, 16) + .padding(.bottom, 16) + .onTapGesture {} + } + } + + if viewModel.isShowPaymentDialog { + SodaDialog( + title: viewModel.paymentDialogTitle, + desc: viewModel.paymentDialogDesc, + confirmButtonTitle: viewModel.paymentDialogConfirmTitle, + confirmButtonAction: viewModel.paymentDialogConfirmAction, + cancelButtonTitle: viewModel.paymentDialogCancelTitle, + cancelButtonAction: viewModel.hidePopup + ) + } + + if viewModel.isShowPasswordDialog { + LiveRoomPasswordDialog( + isShowing: $viewModel.isShowPasswordDialog, + can: viewModel.secretOrPasswordDialogCoin, + confirmAction: viewModel.passwordDialogConfirmAction + ) + } + + if viewModel.isFollowedChannelLoading || + viewModel.isRecommendChannelLoading || + viewModel.isRecommendLiveLoading || + viewModel.isLoading { + LoadingView() + } + } } } diff --git a/SodaLive/Sources/Live/LiveViewModel.swift b/SodaLive/Sources/Live/LiveViewModel.swift new file mode 100644 index 0000000..a30777a --- /dev/null +++ b/SodaLive/Sources/Live/LiveViewModel.swift @@ -0,0 +1,292 @@ +// +// LiveViewModel.swift +// SodaLive +// +// Created by klaus on 2023/08/09. +// + +import Foundation +import Combine + +final class LiveViewModel: ObservableObject { + + private let repository = LiveRepository() + private let eventRepository = EventRepository() + private let liveRecommendRepository = LiveRecommendRepository() + private var subscription = Set() + + @Published private(set) var eventBannerItems = [EventItem]() + @Published private(set) var liveNowItems = [GetRoomListResponse]() + @Published private(set) var liveReservationItems = [GetRoomListResponse]() + @Published private(set) var recommendLiveItems: [GetRecommendLiveResponse] = [] + @Published private(set) var recommendChannelItems: [GetRecommendChannelResponse] = [] + @Published private(set) var followedChannelItems: [GetRecommendChannelResponse] = [] + + @Published var errorMessage = "" + @Published var isShowPopup = false + @Published var isRefresh = false + @Published var isLoading = false + @Published var isRecommendLiveLoading = false + @Published var isRecommendChannelLoading = false + @Published var isFollowedChannelLoading = false + + @Published var paymentDialogTitle = "" + @Published var paymentDialogDesc = "" + @Published var isShowPaymentDialog = false + @Published var paymentDialogConfirmAction = {} + @Published var paymentDialogConfirmTitle = "" + + @Published var secretOrPasswordDialogCoin = 0 + @Published var passwordDialogConfirmAction: (Int) -> Void = { _ in } + @Published var isShowPasswordDialog = false + + @Published var isFollowingList = UserDefaults.bool(forKey: .isFollowedChannel) { + didSet { + UserDefaults.set(isFollowingList, forKey: .isFollowedChannel) + } + } + + let paymentDialogCancelTitle = "취소" + + var page = 1 + var isLast = false + private let pageSize = 10 + + func hidePopup() { + isShowPaymentDialog = false + isShowPasswordDialog = false + + paymentDialogTitle = "" + paymentDialogDesc = "" + paymentDialogConfirmAction = {} + + secretOrPasswordDialogCoin = 0 + + passwordDialogConfirmAction = { _ in } + } + + func getSummary() { + getFollowedChannelList() + getRecommendChannelList() + getRecommendLive() + isLoading = true + + eventBannerItems.removeAll() + liveNowItems.removeAll() + liveReservationItems.removeAll() + + let liveNow = repository.roomList( + request: GetRoomListRequest( + timezone: TimeZone.current.identifier, + dateString: nil, + status: .NOW, + page: 1, + size: 10 + ) + ) + + let liveReservation = repository.roomList( + request: GetRoomListRequest( + timezone: TimeZone.current.identifier, + dateString: nil, + status: .RESERVATION, + page: 1, + size: 10 + ) + ) + + let event = eventRepository.getEvents() + + Publishers + .CombineLatest3(liveNow, liveReservation, event) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { (now, reservation, eventResponse) in + let nowData = now.data + let reservationData = reservation.data + let eventData = eventResponse.data + + let jsonDecoder = JSONDecoder() + + do { + let nowDecoded = try jsonDecoder.decode(ApiResponse<[GetRoomListResponse]>.self, from: nowData) + if let data = nowDecoded.data, nowDecoded.success { + self.liveNowItems.removeAll() + self.liveNowItems.append(contentsOf: data) + } else { + if let message = nowDecoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + + do { + let reservationDecoded = try jsonDecoder.decode(ApiResponse<[GetRoomListResponse]>.self, from: reservationData) + if let data = reservationDecoded.data, reservationDecoded.success { + self.liveReservationItems.removeAll() + self.liveReservationItems.append(contentsOf: data) + } else { + if let message = reservationDecoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + + do { + let eventDecoded = try jsonDecoder.decode(ApiResponse.self, from: eventData) + if let data = eventDecoded.data, eventDecoded.success { + self.eventBannerItems.removeAll() + self.eventBannerItems.append(contentsOf: data.eventList) + } else { + if let message = eventDecoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + + self.isLoading = false + self.isRefresh = false + } + .store(in: &subscription) + } + + private func getFollowedChannelList() { + followedChannelItems.removeAll() + isFollowedChannelLoading = true + + liveRecommendRepository.getFollowedChannelList() + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + self.isFollowedChannelLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse<[GetRecommendChannelResponse]>.self, from: responseData) + + if let data = decoded.data, decoded.success { + self.followedChannelItems.append(contentsOf: data) + } 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 getRecommendChannelList() { + recommendChannelItems.removeAll() + isRecommendChannelLoading = true + + liveRecommendRepository.getRecommendChannelList() + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + self.isRecommendChannelLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse<[GetRecommendChannelResponse]>.self, from: responseData) + + if let data = decoded.data, decoded.success { + self.recommendChannelItems.append(contentsOf: data) + } 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 getRecommendLive() { + recommendLiveItems.removeAll() + isRecommendLiveLoading = true + + liveRecommendRepository.getRecommendLive() + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + self.isRecommendLiveLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse<[GetRecommendLiveResponse]>.self, from: responseData) + + if let data = decoded.data, decoded.success { + self.recommendLiveItems.append(contentsOf: data) + } 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) + } +} diff --git a/SodaLive/Sources/Live/Now/SectionLiveNowView.swift b/SodaLive/Sources/Live/Now/SectionLiveNowView.swift new file mode 100644 index 0000000..b5ad05b --- /dev/null +++ b/SodaLive/Sources/Live/Now/SectionLiveNowView.swift @@ -0,0 +1,31 @@ +// +// SectionLiveNowView.swift +// SodaLive +// +// Created by klaus on 2023/08/09. +// + +import SwiftUI + +struct SectionLiveNowView: View { + + let items: [GetRoomListResponse] + + let onClickParticipant: (Int) -> Void + let onTapCreateLive: () -> Void + + var body: some View { + VStack(spacing: 13.3) { + } + } +} + +struct SectionLiveNowView_Previews: PreviewProvider { + static var previews: some View { + SectionLiveNowView( + items: [], + onClickParticipant: { _ in }, + onTapCreateLive: {} + ) + } +} diff --git a/SodaLive/Sources/Live/Recommend/GetRecommendLiveResponse.swift b/SodaLive/Sources/Live/Recommend/GetRecommendLiveResponse.swift new file mode 100644 index 0000000..60580d6 --- /dev/null +++ b/SodaLive/Sources/Live/Recommend/GetRecommendLiveResponse.swift @@ -0,0 +1,13 @@ +// +// GetRecommendLiveResponse.swift +// SodaLive +// +// Created by klaus on 2023/08/09. +// + +import Foundation + +struct GetRecommendLiveResponse: Decodable, Hashable { + let imageUrl: String + let creatorId: Int +} diff --git a/SodaLive/Sources/Live/Recommend/LiveRecommendApi.swift b/SodaLive/Sources/Live/Recommend/LiveRecommendApi.swift new file mode 100644 index 0000000..82eabad --- /dev/null +++ b/SodaLive/Sources/Live/Recommend/LiveRecommendApi.swift @@ -0,0 +1,56 @@ +// +// LiveRecommendApi.swift +// SodaLive +// +// Created by klaus on 2023/08/09. +// + +import Foundation +import Moya + +enum LiveRecommendApi { + case getFollowedChannelList + case getRecommendChannelList + case getRecommendLive +} + +extension LiveRecommendApi: TargetType { + var baseURL: URL { + return URL(string: BASE_URL)! + } + + var path: String { + switch self { + case .getFollowedChannelList: + return "/live/recommend/following/channel/list" + + case .getRecommendChannelList: + return "/live/recommend/channel" + + case .getRecommendLive: + return "/live/recommend" + } + } + + var method: Moya.Method { + switch self { + case .getFollowedChannelList, + .getRecommendChannelList, + .getRecommendLive: + return .get + } + } + + var task: Moya.Task { + switch self { + case .getFollowedChannelList, + .getRecommendChannelList, + .getRecommendLive: + return .requestPlain + } + } + + var headers: [String : String]? { + return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"] + } +} diff --git a/SodaLive/Sources/Live/Recommend/LiveRecommendRepository.swift b/SodaLive/Sources/Live/Recommend/LiveRecommendRepository.swift new file mode 100644 index 0000000..85e6e7e --- /dev/null +++ b/SodaLive/Sources/Live/Recommend/LiveRecommendRepository.swift @@ -0,0 +1,27 @@ +// +// LiveRecommendRepository.swift +// SodaLive +// +// Created by klaus on 2023/08/09. +// + +import Foundation +import CombineMoya +import Combine +import Moya + +final class LiveRecommendRepository { + private let api = MoyaProvider() + + func getFollowedChannelList() -> AnyPublisher { + return api.requestPublisher(.getFollowedChannelList) + } + + func getRecommendChannelList() -> AnyPublisher { + return api.requestPublisher(.getRecommendChannelList) + } + + func getRecommendLive() -> AnyPublisher { + return api.requestPublisher(.getRecommendLive) + } +} diff --git a/SodaLive/Sources/Live/Recommend/SectionRecommendLiveView.swift b/SodaLive/Sources/Live/Recommend/SectionRecommendLiveView.swift new file mode 100644 index 0000000..ffc3c01 --- /dev/null +++ b/SodaLive/Sources/Live/Recommend/SectionRecommendLiveView.swift @@ -0,0 +1,114 @@ +// +// SectionRecommendLiveView.swift +// SodaLive +// +// Created by klaus on 2023/08/09. +// + +import SwiftUI +import Kingfisher + +struct SectionRecommendLiveView: View { + + let items: [GetRecommendLiveResponse] + @State private var currentIndex = 0 + @State private var timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect() + + var body: some View { + VStack(spacing: 0) { + HStack(spacing: 0) { + Text("추천 ") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color(hex: "ff5c49")) + + Text("라이브") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color(hex: "eeeeee")) + } + .frame(width: screenSize().width - 26.7, alignment: .leading) + + TabView(selection: $currentIndex) { + ForEach(0.. 0 { + Text("\(item.price)코인") + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor( + Color(hex: "e2e2e2") + .opacity(0.5) + ) + + } else { + Text("무료") + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor( + Color(hex: "e2e2e2") + .opacity(0.5) + ) + } + } + } + + Spacer() + + if item.isPrivateRoom { + Image("ic_lock") + .resizable() + .frame(width: 20, height: 20) + } + } + .padding(.vertical, 6.7) + } + + Divider() + .frame(height: 1) + .background(Color(hex: "909090").opacity(0.5)) + } + .frame(width: screenSize().width - 26.7, height: 130, alignment: .center) + } +} + +struct LiveReservationItemView_Previews: PreviewProvider { + static var previews: some View { + LiveReservationItemView(item: GetRoomListResponse( + roomId: 99, + title: "test", + content: "testtest", + beginDateTime: "2022.05.23 Mon 03:00 PM", + numberOfParticipate: 0, + numberOfPeople: 5, + coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", + isAdult: false, + price: 0, + tags: ["팬미팅", "힐링"], + channelName: nil, + managerNickname: "user8", + managerId: 19, + isReservation: false, + isPrivateRoom: true + )) + } +} diff --git a/SodaLive/Sources/Live/Reservation/MyLiveReservationItemView.swift b/SodaLive/Sources/Live/Reservation/MyLiveReservationItemView.swift new file mode 100644 index 0000000..452ac20 --- /dev/null +++ b/SodaLive/Sources/Live/Reservation/MyLiveReservationItemView.swift @@ -0,0 +1,114 @@ +// +// MyLiveReservationItemView.swift +// SodaLive +// +// Created by klaus on 2023/08/10. +// + +import SwiftUI +import Kingfisher + +struct MyLiveReservationItemView: View { + + let item: GetRoomListResponse + let index: Int + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + if index == 0 { + HStack(spacing: 8) { + Image("ic_mic_colored") + + Text("내가 개설한 라이브") + .font(.custom(Font.bold.rawValue, size: 16)) + .foregroundColor(Color(hex: "9970ff")) + } + } + + VStack(alignment: .leading, spacing: 13.3) { + HStack(alignment: .top, spacing: 20) { + ZStack(alignment: .topLeading) { + KFImage(URL(string: item.coverImageUrl)) + .resizable() + .scaledToFill() + .frame(width: 80, height: 116, 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: .center, 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")) + .padding(.top, 4.3) + } + }.frame(height: 116) + + Spacer() + + if item.isPrivateRoom { + Image("ic_lock") + .resizable() + .frame(width: 20, height: 20) + .padding(.trailing, 13.3) + .padding(.top, 13.3) + } + } + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(hex: "9970ff"), lineWidth: 1) + ) + + Divider() + .frame(height: 1) + .background(Color(hex: "909090").opacity(0.5)) + }.frame(width: screenSize().width - 26.7) + } + } +} + +struct MyLiveReservationItemView_Previews: PreviewProvider { + static var previews: some View { + MyLiveReservationItemView( + item: GetRoomListResponse( + roomId: 99, + title: "test", + content: "testtest", + beginDateTime: "2022.05.23 Mon 03:00 PM", + numberOfParticipate: 0, + numberOfPeople: 5, + coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", + isAdult: false, + price: 0, + tags: ["팬미팅", "힐링"], + channelName: nil, + managerNickname: "user8", + managerId: 19, + isReservation: false, + isPrivateRoom: true + ), + index: 0 + ) + } +} diff --git a/SodaLive/Sources/Live/Reservation/SectionLiveReservationView.swift b/SodaLive/Sources/Live/Reservation/SectionLiveReservationView.swift new file mode 100644 index 0000000..f20b83b --- /dev/null +++ b/SodaLive/Sources/Live/Reservation/SectionLiveReservationView.swift @@ -0,0 +1,109 @@ +// +// SectionLiveReservationView.swift +// SodaLive +// +// Created by klaus on 2023/08/09. +// + +import SwiftUI + +struct SectionLiveReservationView: View { + + let items: [GetRoomListResponse] + + let onClickCancel: () -> Void + let onClickStart: (Int) -> Void + let onClickReservation: (Int) -> Void + let onTapCreateLive: () -> Void + + 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: "9970ff")) + + Spacer() + + if items.count > 0 { + Text("전체보기") + .font(.custom(Font.light.rawValue, size: 11.3)) + .foregroundColor(Color(hex: "bbbbbb")) + .onTapGesture {} + } + } + .padding(.horizontal, 13.3) + .frame(width: screenSize().width) + + if items.count > 0 { + VStack(spacing: 13.3) { + ForEach(0..() + + func getEvents() -> AnyPublisher { + return api.requestPublisher(.getEvents) + } + + func getEventPopup() -> AnyPublisher { + return api.requestPublisher(.getEventPopup) + } +} +