feat(content): 추천 오디오 탭을 추가한다
This commit is contained in:
@@ -54,7 +54,13 @@ struct CreatorChannelAudioTabView: View {
|
||||
}
|
||||
|
||||
ForEach(viewModel.audioContents) { audioContent in
|
||||
CreatorChannelAudioContentListItem(audioContent: audioContent) {
|
||||
AudioContentListRow(
|
||||
item: AudioContentListRowItem(
|
||||
audioContent: audioContent,
|
||||
subtitle: audioContent.duration
|
||||
),
|
||||
style: .purchaseState
|
||||
) {
|
||||
onTapContent(audioContent.audioContentId)
|
||||
}
|
||||
.onAppear {
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct CreatorChannelAudioContentListItem: View {
|
||||
let audioContent: CreatorChannelAudioContentResponse
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
audioContent: CreatorChannelAudioContentResponse,
|
||||
action: @escaping () -> Void = {}
|
||||
) {
|
||||
self.audioContent = audioContent
|
||||
self.action = action
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
action()
|
||||
} label: {
|
||||
HStack(alignment: .center, spacing: SodaSpacing.s14) {
|
||||
thumbnail
|
||||
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s6) {
|
||||
Text(audioContent.title)
|
||||
.appFont(size: 16, weight: .bold)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(2)
|
||||
.truncationMode(.tail)
|
||||
|
||||
if let duration = audioContent.duration, !duration.isEmpty {
|
||||
Text(duration)
|
||||
.appFont(size: 14, weight: .medium)
|
||||
.foregroundColor(Color.gray500)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
trailingState
|
||||
}
|
||||
.padding(.horizontal, SodaSpacing.s20)
|
||||
.padding(.vertical, SodaSpacing.s8)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private var thumbnail: some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
DownsampledKFImage(
|
||||
url: audioContent.imageUrl.flatMap(URL.init(string:)),
|
||||
size: CGSize(width: 88, height: 88)
|
||||
)
|
||||
.background(Color.gray800)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
topTagList
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
bottomTagList
|
||||
}
|
||||
}
|
||||
.frame(width: 88, height: 88)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var topTagList: some View {
|
||||
HStack(spacing: 0) {
|
||||
if audioContent.isOriginalSeries == true {
|
||||
CreatorChannelAudioImageTag(imageName: "ic_content_tag_original")
|
||||
}
|
||||
|
||||
if audioContent.isFirstContent {
|
||||
CreatorChannelAudioFirstTag()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if audioContent.isAdult {
|
||||
CreatorChannelAudioAdultTag()
|
||||
.padding(.top, 6)
|
||||
.padding(.trailing, 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var bottomTagList: some View {
|
||||
HStack(spacing: 0) {
|
||||
if audioContent.isPointAvailable {
|
||||
CreatorChannelAudioImageTag(imageName: "ic_content_tag_point")
|
||||
}
|
||||
|
||||
if audioContent.price == 0 {
|
||||
CreatorChannelAudioFreeTag()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var trailingState: some View {
|
||||
if audioContent.isOwned {
|
||||
playbackState(I18n.Content.Status.owned)
|
||||
} else if audioContent.isRented {
|
||||
playbackState(I18n.Content.Status.rented)
|
||||
} else if audioContent.price == 0 {
|
||||
playbackState("")
|
||||
} else {
|
||||
HStack(spacing: 2) {
|
||||
Image("ic_bar_cash")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 18, height: 18)
|
||||
|
||||
Text(audioContent.price.comma())
|
||||
.appFont(size: 14, weight: .regular)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func playbackState(_ title: String) -> some View {
|
||||
VStack(spacing: SodaSpacing.s4) {
|
||||
ZStack {
|
||||
Color.white
|
||||
|
||||
Image("ic_new_player_play")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 16, height: 16)
|
||||
}
|
||||
.frame(width: 28, height: 28)
|
||||
.clipShape(Circle())
|
||||
|
||||
if !title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
Text(title)
|
||||
.appFont(size: 12, weight: .medium)
|
||||
.foregroundColor(Color.gray400)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CreatorChannelAudioContentListItem_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack(spacing: SodaSpacing.s8) {
|
||||
CreatorChannelAudioContentListItem(
|
||||
audioContent: CreatorChannelAudioContentResponse(
|
||||
audioContentId: 1,
|
||||
title: "라이브 다시듣기 제목이 두 줄까지 표시됩니다",
|
||||
duration: "1:43:25",
|
||||
imageUrl: "https://picsum.photos/300/300",
|
||||
price: 10,
|
||||
isAdult: true,
|
||||
isPointAvailable: true,
|
||||
isFirstContent: true,
|
||||
seriesName: nil,
|
||||
isOriginalSeries: true,
|
||||
isOwned: true,
|
||||
isRented: false
|
||||
)
|
||||
)
|
||||
|
||||
CreatorChannelAudioContentListItem(
|
||||
audioContent: CreatorChannelAudioContentResponse(
|
||||
audioContentId: 1,
|
||||
title: "라이브 다시듣기 제목이 두 줄까지 표시됩니다",
|
||||
duration: "1:43:25",
|
||||
imageUrl: "https://picsum.photos/300/300",
|
||||
price: 10,
|
||||
isAdult: true,
|
||||
isPointAvailable: true,
|
||||
isFirstContent: true,
|
||||
seriesName: nil,
|
||||
isOriginalSeries: true,
|
||||
isOwned: false,
|
||||
isRented: true
|
||||
)
|
||||
)
|
||||
|
||||
CreatorChannelAudioContentListItem(
|
||||
audioContent: CreatorChannelAudioContentResponse(
|
||||
audioContentId: 1,
|
||||
title: "라이브 다시듣기 제목이 두 줄까지 표시됩니다",
|
||||
duration: "1:43:25",
|
||||
imageUrl: "https://picsum.photos/300/300",
|
||||
price: 3000,
|
||||
isAdult: true,
|
||||
isPointAvailable: true,
|
||||
isFirstContent: true,
|
||||
seriesName: nil,
|
||||
isOriginalSeries: true,
|
||||
isOwned: false,
|
||||
isRented: false
|
||||
)
|
||||
)
|
||||
|
||||
CreatorChannelAudioContentListItem(
|
||||
audioContent: CreatorChannelAudioContentResponse(
|
||||
audioContentId: 1,
|
||||
title: "라이브 다시듣기 제목이 두 줄까지 표시됩니다",
|
||||
duration: "1:43:25",
|
||||
imageUrl: "https://picsum.photos/300/300",
|
||||
price: 0,
|
||||
isAdult: true,
|
||||
isPointAvailable: true,
|
||||
isFirstContent: true,
|
||||
seriesName: nil,
|
||||
isOriginalSeries: true,
|
||||
isOwned: false,
|
||||
isRented: false
|
||||
)
|
||||
)
|
||||
}
|
||||
.background(Color.black)
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,10 @@ struct CreatorChannelAudioSection: View {
|
||||
private let homeVisibleCount = 9
|
||||
private let itemsPerColumn = 3
|
||||
|
||||
private var rowWidth: CGFloat {
|
||||
AudioContentListRowStyle.compactWidth()
|
||||
}
|
||||
|
||||
init(
|
||||
audioContents: [CreatorChannelAudioContentResponse],
|
||||
onSelectTab: @escaping (CreatorChannelTab) -> Void,
|
||||
@@ -30,7 +34,13 @@ struct CreatorChannelAudioSection: View {
|
||||
ForEach(audioContentColumns.indices, id: \.self) { index in
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s8) {
|
||||
ForEach(audioContentColumns[index]) { audioContent in
|
||||
CreatorChannelHomeAudioContentListItem(audioContent: audioContent) {
|
||||
AudioContentListRow(
|
||||
item: AudioContentListRowItem(
|
||||
audioContent: audioContent,
|
||||
subtitle: subtitle(for: audioContent)
|
||||
),
|
||||
style: .compact(width: rowWidth)
|
||||
) {
|
||||
onTapContent(audioContent.audioContentId)
|
||||
}
|
||||
}
|
||||
@@ -54,6 +64,17 @@ struct CreatorChannelAudioSection: View {
|
||||
Array(visibleAudioContents[index..<min(index + itemsPerColumn, visibleAudioContents.count)])
|
||||
}
|
||||
}
|
||||
|
||||
private func subtitle(for audioContent: CreatorChannelAudioContentResponse) -> String? {
|
||||
let subtitle = [audioContent.duration, audioContent.seriesName]
|
||||
.compactMap { value in
|
||||
guard let value, !value.isEmpty else { return nil }
|
||||
return value
|
||||
}
|
||||
.joined(separator: " • ")
|
||||
|
||||
return subtitle.isEmpty ? nil : subtitle
|
||||
}
|
||||
}
|
||||
|
||||
struct CreatorChannelAudioSection_Previews: PreviewProvider {
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct CreatorChannelHomeAudioContentListItem: View {
|
||||
let audioContent: CreatorChannelAudioContentResponse
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
audioContent: CreatorChannelAudioContentResponse,
|
||||
action: @escaping () -> Void = {}
|
||||
) {
|
||||
self.audioContent = audioContent
|
||||
self.action = action
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack(alignment: .center, spacing: SodaSpacing.s14) {
|
||||
thumbnail
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(audioContent.title)
|
||||
.appFont(size: 16, weight: .bold)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
|
||||
if !subtitle.isEmpty {
|
||||
Text(subtitle)
|
||||
.appFont(size: 14, weight: .medium)
|
||||
.foregroundColor(Color.gray500)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.frame(width: 346, alignment: .leading)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private var subtitle: String {
|
||||
[audioContent.duration, audioContent.seriesName]
|
||||
.compactMap { value in
|
||||
guard let value, !value.isEmpty else { return nil }
|
||||
return value
|
||||
}
|
||||
.joined(separator: " • ")
|
||||
}
|
||||
|
||||
private var thumbnail: some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
DownsampledKFImage(
|
||||
url: audioContent.imageUrl.flatMap(URL.init(string:)),
|
||||
size: CGSize(width: 88, height: 88)
|
||||
)
|
||||
.background(Color.gray800)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
topTagList
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
bottomTagList
|
||||
}
|
||||
}
|
||||
.frame(width: 88, height: 88)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var topTagList: some View {
|
||||
HStack(spacing: 0) {
|
||||
if audioContent.isOriginalSeries == true {
|
||||
CreatorChannelAudioImageTag(imageName: "ic_content_tag_original")
|
||||
}
|
||||
|
||||
if audioContent.isFirstContent {
|
||||
CreatorChannelAudioFirstTag()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var bottomTagList: some View {
|
||||
HStack(spacing: 0) {
|
||||
if audioContent.isPointAvailable {
|
||||
CreatorChannelAudioImageTag(imageName: "ic_content_tag_point")
|
||||
}
|
||||
|
||||
if audioContent.price == 0 {
|
||||
CreatorChannelAudioFreeTag()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CreatorChannelAudioImageTag: View {
|
||||
let imageName: String
|
||||
|
||||
var body: some View {
|
||||
Image(imageName)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 24, height: 24)
|
||||
}
|
||||
}
|
||||
|
||||
struct CreatorChannelAudioFirstTag: View {
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color(hex: "FF34B8")
|
||||
|
||||
Image("ic_content_tag_first_star")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.padding(SodaSpacing.s4)
|
||||
}
|
||||
.frame(width: 24, height: 24)
|
||||
}
|
||||
}
|
||||
|
||||
struct CreatorChannelAudioAdultTag: View {
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color(hex: "FF4C3C")
|
||||
|
||||
Image("ic_new_shield_small")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.padding(2)
|
||||
}
|
||||
.frame(width: 18, height: 18)
|
||||
.cornerRadius(4)
|
||||
}
|
||||
}
|
||||
|
||||
struct CreatorChannelAudioFreeTag: View {
|
||||
var body: some View {
|
||||
Text("무료")
|
||||
.appFont(size: 14, weight: .semibold)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
.padding(SodaSpacing.s4)
|
||||
.frame(height: 24)
|
||||
.background(Color.soda900)
|
||||
}
|
||||
}
|
||||
|
||||
struct CreatorChannelHomeAudioContentListItem_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CreatorChannelHomeAudioContentListItem(
|
||||
audioContent: CreatorChannelAudioContentResponse(
|
||||
audioContentId: 1,
|
||||
title: "콘텐츠 이름이 한 줄 콘텐츠 이름이 한 줄 콘텐츠 이름이 한 줄",
|
||||
duration: "3:00",
|
||||
imageUrl: "https://picsum.photos/300/300",
|
||||
price: 0,
|
||||
isAdult: false,
|
||||
isPointAvailable: true,
|
||||
isFirstContent: true,
|
||||
seriesName: "시리즈 이름",
|
||||
isOriginalSeries: true,
|
||||
isOwned: false,
|
||||
isRented: false
|
||||
)
|
||||
)
|
||||
.padding(SodaSpacing.s14)
|
||||
.background(Color.black)
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,13 @@ struct CreatorChannelLiveTabView: View {
|
||||
.padding(.bottom, SodaSpacing.s12)
|
||||
|
||||
ForEach(viewModel.liveReplayContents) { audioContent in
|
||||
CreatorChannelAudioContentListItem(audioContent: audioContent) {
|
||||
AudioContentListRow(
|
||||
item: AudioContentListRowItem(
|
||||
audioContent: audioContent,
|
||||
subtitle: audioContent.duration
|
||||
),
|
||||
style: .purchaseState
|
||||
) {
|
||||
onTapContent(audioContent.audioContentId)
|
||||
}
|
||||
.onAppear {
|
||||
|
||||
Reference in New Issue
Block a user