재생목록 상세 페이지 추가

This commit is contained in:
Yu Sung 2024-12-10 03:01:40 +09:00
parent 0fdb9edd23
commit d03fee372a
15 changed files with 447 additions and 5 deletions

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 B

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 B

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 B

View File

@ -11,9 +11,12 @@ struct ContentBoxView: View {
@StateObject var viewModel = ContentBoxViewModel() @StateObject var viewModel = ContentBoxViewModel()
@State private var isShowDetailPlaylist = false
@State private var isShowCreatePlaylist = false @State private var isShowCreatePlaylist = false
@State private var isReloadData = false @State private var isReloadData = false
@State private var selectedPlaylistId = 0
var body: some View { var body: some View {
ZStack { ZStack {
NavigationView { NavigationView {
@ -55,7 +58,11 @@ struct ContentBoxView: View {
} }
} else { } else {
ContentPlaylistListView( ContentPlaylistListView(
onClickCreate: { isShowCreatePlaylist = true } onClickCreate: { isShowCreatePlaylist = true },
onClickItem: { playlistId in
selectedPlaylistId = playlistId
isShowDetailPlaylist = true
}
) )
.padding(.bottom, 13.3) .padding(.bottom, 13.3)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
@ -72,6 +79,14 @@ struct ContentBoxView: View {
reloadData: $isReloadData reloadData: $isReloadData
) )
} }
if isShowDetailPlaylist {
ContentPlaylistDetailView(
playlistId: selectedPlaylistId,
isShowing: $isShowDetailPlaylist,
reloadData: $isReloadData
)
}
} }
} }
} }

View File

@ -19,4 +19,8 @@ class ContentPlaylistListRepository {
func createPlaylist(request: CreatePlaylistRequest) -> AnyPublisher<Response, MoyaError> { func createPlaylist(request: CreatePlaylistRequest) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.createPlaylist(request: request)) return api.requestPublisher(.createPlaylist(request: request))
} }
func getPlaylistDetail(playlistId: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getPlaylistDetail(playlistId: playlistId))
}
} }

View File

@ -9,9 +9,10 @@ import SwiftUI
struct ContentPlaylistListView: View { struct ContentPlaylistListView: View {
@ObservedObject var viewModel = ContentPlaylistListViewModel() @StateObject var viewModel = ContentPlaylistListViewModel()
let onClickCreate: () -> Void let onClickCreate: () -> Void
let onClickItem: (Int) -> Void
var body: some View { var body: some View {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
@ -61,6 +62,10 @@ struct ContentPlaylistListView: View {
ForEach(0..<viewModel.playlists.count, id: \.self) { index in ForEach(0..<viewModel.playlists.count, id: \.self) { index in
let playlist = viewModel.playlists[index] let playlist = viewModel.playlists[index]
ContentPlaylistItemView(item: playlist) ContentPlaylistItemView(item: playlist)
.contentShape(Rectangle())
.onTapGesture {
onClickItem(playlist.id)
}
} }
} }
} }
@ -89,5 +94,5 @@ struct ContentPlaylistListView: View {
} }
#Preview { #Preview {
ContentPlaylistListView(onClickCreate: {}) ContentPlaylistListView(onClickCreate: {}, onClickItem: { _ in })
} }

View File

@ -0,0 +1,197 @@
//
// ContentPlaylistDetailView.swift
// SodaLive
//
// Created by klaus on 12/9/24.
//
import SwiftUI
import Kingfisher
struct ContentPlaylistDetailView: View {
@StateObject var viewModel = ContentPlaylistDetailViewModel()
let playlistId: Int
@Binding var isShowing: Bool
@Binding var reloadData: Bool
var body: some View {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 21.3) {
HStack(spacing: 5.3) {
Image("ic_back")
.resizable()
.frame(width: 20, height: 20)
.padding(8)
.onTapGesture {
isShowing = false
}
Spacer()
Image("ic_edit_white")
.padding(8)
.onTapGesture {
}
Image("ic_seemore_vertical_white")
.padding(8)
.onTapGesture {
}
}
.padding(.horizontal, 13.3)
.frame(height: 50)
.frame(maxWidth: .infinity)
.background(Color.black)
ScrollView(.vertical, showsIndicators: false) {
if let response = viewModel.response {
VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .top, spacing: 13.3) {
VStack(alignment: .center, spacing: 0) {
HStack(spacing: 0) {
KFImage(URL(string: response.playlistCoverImageList[0]))
.cancelOnDisappear(true)
.downsampling(size: CGSize(width: 80, height: 80))
.resizable()
.scaledToFill()
.clipped()
.frame(maxWidth: .infinity, maxHeight: .infinity)
if response.playlistCoverImageList.count > 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..<response.contentList.count, id: \.self) {
PlaylistContentItemView(item: response.contentList[$0])
}
}
.padding(.top, 18)
}
.padding(.horizontal, 13.3)
}
}
}
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
.onAppear {
viewModel.playlistId = playlistId
}
}
}
}
#Preview {
ContentPlaylistDetailView(
playlistId: 1,
isShowing: .constant(true),
reloadData: .constant(false)
)
}

View File

@ -0,0 +1,67 @@
//
// ContentPlaylistDetailViewModel.swift
// SodaLive
//
// Created by klaus on 12/9/24.
//
import Foundation
import Combine
final class ContentPlaylistDetailViewModel: ObservableObject {
private let repository = ContentPlaylistListRepository()
private var subscription = Set<AnyCancellable>()
@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<GetPlaylistDetailResponse>.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)
}
}

View File

@ -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"
)
)
}

View File

@ -11,6 +11,7 @@ import Moya
enum PlaylistApi { enum PlaylistApi {
case getPlaylistList case getPlaylistList
case createPlaylist(request: CreatePlaylistRequest) case createPlaylist(request: CreatePlaylistRequest)
case getPlaylistDetail(playlistId: Int)
} }
extension PlaylistApi: TargetType { extension PlaylistApi: TargetType {
@ -22,12 +23,15 @@ extension PlaylistApi: TargetType {
switch self { switch self {
case .getPlaylistList, .createPlaylist: case .getPlaylistList, .createPlaylist:
return "/audio-content/playlist" return "/audio-content/playlist"
case .getPlaylistDetail(let playlistId):
return "/audio-content/playlist/\(playlistId)"
} }
} }
var method: Moya.Method { var method: Moya.Method {
switch self { switch self {
case .getPlaylistList: case .getPlaylistList, .getPlaylistDetail:
return .get return .get
case .createPlaylist: case .createPlaylist:
@ -37,7 +41,7 @@ extension PlaylistApi: TargetType {
var task: Moya.Task { var task: Moya.Task {
switch self { switch self {
case .getPlaylistList: case .getPlaylistList, .getPlaylistDetail:
return .requestPlain return .requestPlain
case .createPlaylist(let request): case .createPlaylist(let request):