diff --git a/SodaLive/Resources/Assets.xcassets/ic_edit_white.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_edit_white.imageset/Contents.json new file mode 100644 index 0000000..180a976 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_edit_white.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_edit_white.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_edit_white.imageset/ic_edit_white.png b/SodaLive/Resources/Assets.xcassets/ic_edit_white.imageset/ic_edit_white.png new file mode 100644 index 0000000..9e0d7e7 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_edit_white.imageset/ic_edit_white.png differ diff --git a/SodaLive/Resources/Assets.xcassets/ic_playlist_play.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_playlist_play.imageset/Contents.json new file mode 100644 index 0000000..26e65bc --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_playlist_play.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_playlist_play.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_playlist_play.imageset/ic_playlist_play.png b/SodaLive/Resources/Assets.xcassets/ic_playlist_play.imageset/ic_playlist_play.png new file mode 100644 index 0000000..a5c642d Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_playlist_play.imageset/ic_playlist_play.png differ diff --git a/SodaLive/Resources/Assets.xcassets/ic_playlist_shuffle.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_playlist_shuffle.imageset/Contents.json new file mode 100644 index 0000000..9b2410f --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_playlist_shuffle.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_playlist_shuffle.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_playlist_shuffle.imageset/ic_playlist_shuffle.png b/SodaLive/Resources/Assets.xcassets/ic_playlist_shuffle.imageset/ic_playlist_shuffle.png new file mode 100644 index 0000000..56fa8d4 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_playlist_shuffle.imageset/ic_playlist_shuffle.png differ diff --git a/SodaLive/Resources/Assets.xcassets/ic_seemore_vertical_white.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_seemore_vertical_white.imageset/Contents.json new file mode 100644 index 0000000..8106340 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_seemore_vertical_white.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_seemore_vertical_white.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_seemore_vertical_white.imageset/ic_seemore_vertical_white.png b/SodaLive/Resources/Assets.xcassets/ic_seemore_vertical_white.imageset/ic_seemore_vertical_white.png new file mode 100644 index 0000000..69c0cd6 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_seemore_vertical_white.imageset/ic_seemore_vertical_white.png differ diff --git a/SodaLive/Sources/Content/Box/ContentBoxView.swift b/SodaLive/Sources/Content/Box/ContentBoxView.swift index 5dd9ee7..8f0e074 100644 --- a/SodaLive/Sources/Content/Box/ContentBoxView.swift +++ b/SodaLive/Sources/Content/Box/ContentBoxView.swift @@ -11,9 +11,12 @@ struct ContentBoxView: View { @StateObject var viewModel = ContentBoxViewModel() + @State private var isShowDetailPlaylist = false @State private var isShowCreatePlaylist = false @State private var isReloadData = false + @State private var selectedPlaylistId = 0 + var body: some View { ZStack { NavigationView { @@ -55,7 +58,11 @@ struct ContentBoxView: View { } } else { ContentPlaylistListView( - onClickCreate: { isShowCreatePlaylist = true } + onClickCreate: { isShowCreatePlaylist = true }, + onClickItem: { playlistId in + selectedPlaylistId = playlistId + isShowDetailPlaylist = true + } ) .padding(.bottom, 13.3) .padding(.horizontal, 13.3) @@ -72,6 +79,14 @@ struct ContentBoxView: View { reloadData: $isReloadData ) } + + if isShowDetailPlaylist { + ContentPlaylistDetailView( + playlistId: selectedPlaylistId, + isShowing: $isShowDetailPlaylist, + reloadData: $isReloadData + ) + } } } } diff --git a/SodaLive/Sources/Content/Playlist/ContentPlaylistListRepository.swift b/SodaLive/Sources/Content/Playlist/ContentPlaylistListRepository.swift index ae739de..0724c62 100644 --- a/SodaLive/Sources/Content/Playlist/ContentPlaylistListRepository.swift +++ b/SodaLive/Sources/Content/Playlist/ContentPlaylistListRepository.swift @@ -19,4 +19,8 @@ class ContentPlaylistListRepository { func createPlaylist(request: CreatePlaylistRequest) -> AnyPublisher { return api.requestPublisher(.createPlaylist(request: request)) } + + func getPlaylistDetail(playlistId: Int) -> AnyPublisher { + return api.requestPublisher(.getPlaylistDetail(playlistId: playlistId)) + } } diff --git a/SodaLive/Sources/Content/Playlist/ContentPlaylistListView.swift b/SodaLive/Sources/Content/Playlist/ContentPlaylistListView.swift index 982c4d5..2009265 100644 --- a/SodaLive/Sources/Content/Playlist/ContentPlaylistListView.swift +++ b/SodaLive/Sources/Content/Playlist/ContentPlaylistListView.swift @@ -9,9 +9,10 @@ import SwiftUI struct ContentPlaylistListView: View { - @ObservedObject var viewModel = ContentPlaylistListViewModel() + @StateObject var viewModel = ContentPlaylistListViewModel() let onClickCreate: () -> Void + let onClickItem: (Int) -> Void var body: some View { BaseView(isLoading: $viewModel.isLoading) { @@ -61,6 +62,10 @@ struct ContentPlaylistListView: View { ForEach(0.. 2 { + KFImage(URL(string: response.playlistCoverImageList[1])) + .cancelOnDisappear(true) + .downsampling(size: CGSize(width: 80, height: 80)) + .resizable() + .scaledToFill() + .clipped() + .frame(maxWidth: 40, maxHeight: 40) + } + } + + HStack(spacing: 0) { + if response.playlistCoverImageList.count > 2 { + KFImage(URL(string: response.playlistCoverImageList[2])) + .cancelOnDisappear(true) + .downsampling(size: CGSize(width: 80, height: 80)) + .resizable() + .scaledToFill() + .clipped() + .frame(maxWidth: 40, maxHeight: 40) + } + + if response.playlistCoverImageList.count > 3 { + KFImage(URL(string: response.playlistCoverImageList[3])) + .cancelOnDisappear(true) + .downsampling(size: CGSize(width: 80, height: 80)) + .resizable() + .scaledToFill() + .clipped() + .frame(maxWidth: 40, maxHeight: 40) + } + } + } + .frame(width: 80, height: 80) + .background(Color.graybb) + .cornerRadius(4) + + VStack(spacing: 6.7) { + Text(response.title) + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color.grayd2) + .lineLimit(2) + .truncationMode(.tail) + + Text(response.desc.prefix(100)) + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color.gray90) + .truncationMode(.tail) + } + } + + HStack(spacing: 0) { + Text("만든 날짜 \(response.createdDate)") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color.gray90) + + Spacer() + + Text("\(response.contentCount)개") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color.grayee) + } + .padding(.top, 13.3) + + HStack(spacing: 13.3) { + HStack(spacing: 5.3) { + Image("ic_playlist_play") + + Text("Play") + .font(.custom(Font.bold.rawValue, size: 14.7)) + .foregroundColor(Color.white) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 11) + .background(Color.button) + .cornerRadius(5.3) + .contentShape(Rectangle()) + .onTapGesture { + } + + HStack(spacing: 5.3) { + Image("ic_playlist_shuffle") + + Text("Shuffle") + .font(.custom(Font.bold.rawValue, size: 14.7)) + .foregroundColor(Color.white) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 11) + .background(Color.button) + .cornerRadius(5.3) + .contentShape(Rectangle()) + .onTapGesture { + } + } + .padding(.top, 18) + + LazyVStack(alignment: .leading, spacing: 0) { + ForEach(0..() + + @Published var isLoading = false + @Published var errorMessage = "" + @Published var isShowPopup = false + + @Published var response: GetPlaylistDetailResponse? = nil + + var playlistId: Int = 0 { + didSet { + if playlistId > 0 { + getPlaylistDetail() + } + } + } + + private func getPlaylistDetail() { + isLoading = true + + repository.getPlaylistDetail(playlistId: playlistId) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] 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 { + self.response = 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) + } +} diff --git a/SodaLive/Sources/Content/Playlist/Detail/PlaylistContentItemView.swift b/SodaLive/Sources/Content/Playlist/Detail/PlaylistContentItemView.swift new file mode 100644 index 0000000..4524c63 --- /dev/null +++ b/SodaLive/Sources/Content/Playlist/Detail/PlaylistContentItemView.swift @@ -0,0 +1,66 @@ +// +// PlaylistContentItemView.swift +// SodaLive +// +// Created by klaus on 12/10/24. +// + +import SwiftUI +import Kingfisher + +struct PlaylistContentItemView: View { + + let item: AudioContentPlaylistContent + + var body: some View { + HStack(spacing: 13.3) { + KFImage(URL(string: item.coverUrl)) + .cancelOnDisappear(true) + .downsampling(size: CGSize(width: 40, height: 40)) + .resizable() + .scaledToFill() + .frame(width: 40, height: 40, alignment: .center) + .cornerRadius(5.3) + .clipped() + .padding(.vertical, 7.5) + + VStack(alignment: .leading, spacing: 2.6) { + HStack(spacing: 8) { + Text(item.category) + .font(.custom(Font.medium.rawValue, size: 10)) + .foregroundColor(Color(hex: "3bac6a")) + .padding(2.6) + .background(Color(hex: "28312b")) + .cornerRadius(2.6) + + Text(item.duration) + .font(.custom(Font.medium.rawValue, size: 10)) + .foregroundColor(Color.gray77) + .padding(2.6) + .background(Color.gray22) + .cornerRadius(2.6) + } + + Text(item.title) + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color.grayd2) + .lineLimit(2) + .truncationMode(.tail) + } + } + } +} + +#Preview { + PlaylistContentItemView( + item: AudioContentPlaylistContent( + id: 1, + title: "안녕하세요 오늘은 커버곡을 들려드릴께요....안녕하세요 오늘은 커버곡을 들려드릴께요....", + category: "커버곡", + coverUrl: "https://test-cf.sodalive.net/profile/default-profile.png", + duration: "00:30:20", + creatorNickname: "유저1", + creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png" + ) + ) +} diff --git a/SodaLive/Sources/Content/Playlist/PlaylistApi.swift b/SodaLive/Sources/Content/Playlist/PlaylistApi.swift index 23a05b4..ff4a592 100644 --- a/SodaLive/Sources/Content/Playlist/PlaylistApi.swift +++ b/SodaLive/Sources/Content/Playlist/PlaylistApi.swift @@ -11,6 +11,7 @@ import Moya enum PlaylistApi { case getPlaylistList case createPlaylist(request: CreatePlaylistRequest) + case getPlaylistDetail(playlistId: Int) } extension PlaylistApi: TargetType { @@ -22,12 +23,15 @@ extension PlaylistApi: TargetType { switch self { case .getPlaylistList, .createPlaylist: return "/audio-content/playlist" + + case .getPlaylistDetail(let playlistId): + return "/audio-content/playlist/\(playlistId)" } } var method: Moya.Method { switch self { - case .getPlaylistList: + case .getPlaylistList, .getPlaylistDetail: return .get case .createPlaylist: @@ -37,7 +41,7 @@ extension PlaylistApi: TargetType { var task: Moya.Task { switch self { - case .getPlaylistList: + case .getPlaylistList, .getPlaylistDetail: return .requestPlain case .createPlaylist(let request):