feat(content): 추천 오디오 탭을 추가한다
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MainContentAudioBannerSection: View {
|
||||
let items: [AudioBannerResponse]
|
||||
let onTapBanner: (AudioBannerResponse) -> Void
|
||||
|
||||
var body: some View {
|
||||
BannerCarouselSection(
|
||||
items: items,
|
||||
id: fallbackId,
|
||||
imageUrl: { $0.imageUrl },
|
||||
action: onTapBanner
|
||||
)
|
||||
}
|
||||
|
||||
private func fallbackId(for item: AudioBannerResponse, index: Int) -> String {
|
||||
if let eventItem = item.eventItem {
|
||||
return "content-banner-\(index)-event-\(eventItem.id)"
|
||||
}
|
||||
|
||||
if let creatorId = item.creatorId {
|
||||
return "content-banner-\(index)-creator-\(creatorId)"
|
||||
}
|
||||
|
||||
if let seriesId = item.seriesId {
|
||||
return "content-banner-\(index)-series-\(seriesId)"
|
||||
}
|
||||
|
||||
if let link = item.link, !link.isEmpty {
|
||||
return "content-banner-\(index)-link-\(link)"
|
||||
}
|
||||
|
||||
return "content-banner-\(index)-image-\(item.imageUrl)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MainContentAudioEmptyStateView: View {
|
||||
let message: String
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: SodaSpacing.s12) {
|
||||
Image(systemName: "waveform")
|
||||
.font(.system(size: 32, weight: .semibold))
|
||||
.foregroundColor(Color.gray600)
|
||||
|
||||
Text(message)
|
||||
.appFont(.body1)
|
||||
.foregroundColor(Color.gray400)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding(SodaSpacing.s20)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MainContentAudioHorizontalCardSection: View {
|
||||
let title: String
|
||||
let items: [AudioCardResponse]
|
||||
let onTapContent: (Int) -> Void
|
||||
|
||||
var body: some View {
|
||||
if !items.isEmpty {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
|
||||
SectionTitle(title: title)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .top, spacing: SodaSpacing.s14) {
|
||||
ForEach(items) { item in
|
||||
Button {
|
||||
onTapContent(item.audioContentId)
|
||||
} label: {
|
||||
AudioContentThumbnailCard(
|
||||
item: AudioContentThumbnailCardItem(audioCard: item),
|
||||
size: .medium
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, SodaSpacing.s20)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MainContentAudioListCarouselSection: View {
|
||||
let title: String
|
||||
let items: [AudioCardResponse]
|
||||
let onTapContent: (Int) -> Void
|
||||
|
||||
private var chunks: [[AudioCardResponse]] {
|
||||
stride(from: 0, to: items.count, by: 3).map {
|
||||
Array(items[$0..<min($0 + 3, items.count)])
|
||||
}
|
||||
}
|
||||
|
||||
private var rowWidth: CGFloat {
|
||||
AudioContentListRowStyle.compactWidth()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if !items.isEmpty {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
|
||||
SectionTitle(title: title)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .top, spacing: SodaSpacing.s14) {
|
||||
ForEach(Array(chunks.enumerated()), id: \.offset) { _, chunk in
|
||||
VStack(spacing: SodaSpacing.s12) {
|
||||
ForEach(chunk) { item in
|
||||
AudioContentListRow(
|
||||
item: rowItem(from: item),
|
||||
style: .compact(width: rowWidth, showsAdultTag: true)
|
||||
) {
|
||||
onTapContent(item.audioContentId)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: rowWidth)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, SodaSpacing.s20)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func rowItem(from item: AudioCardResponse) -> AudioContentListRowItem {
|
||||
AudioContentListRowItem(
|
||||
id: item.audioContentId,
|
||||
title: item.title,
|
||||
subtitle: item.creatorNickname,
|
||||
imageUrl: item.imageUrl,
|
||||
price: item.price,
|
||||
isAdult: item.isAdult,
|
||||
isPointAvailable: item.isPointAvailable,
|
||||
isFirstContent: item.isFirstContent,
|
||||
isOriginalSeries: item.isOriginalSeries,
|
||||
isOwned: false,
|
||||
isRented: false
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MainContentMostCommentedAudioSection: View {
|
||||
let title: String
|
||||
let items: [CommentedAudioResponse]
|
||||
let onTapContent: (Int) -> Void
|
||||
|
||||
private let thumbnailSize: CGFloat = 88
|
||||
|
||||
private var cardWidth: CGFloat {
|
||||
UIScreen.main.bounds.width - (SodaSpacing.s14 * 2)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if !items.isEmpty {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
|
||||
SectionTitle(title: title)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .top, spacing: SodaSpacing.s4) {
|
||||
ForEach(items) { item in
|
||||
card(item)
|
||||
}
|
||||
}
|
||||
.padding(.leading, SodaSpacing.s14)
|
||||
.padding(.trailing, SodaSpacing.s20)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func card(_ item: CommentedAudioResponse) -> some View {
|
||||
Button {
|
||||
onTapContent(item.audioContentId)
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
|
||||
audioSummary(item)
|
||||
|
||||
commentPreview(item)
|
||||
}
|
||||
.padding(SodaSpacing.s14)
|
||||
.frame(width: cardWidth, alignment: .leading)
|
||||
.background(Color.gray900)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private func audioSummary(_ item: CommentedAudioResponse) -> some View {
|
||||
HStack(alignment: .center, spacing: SodaSpacing.s14) {
|
||||
DownsampledKFImage(
|
||||
url: URL(string: item.imageUrl),
|
||||
size: CGSize(width: thumbnailSize, height: thumbnailSize)
|
||||
)
|
||||
.background(Color.gray800)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||
|
||||
Text(item.title)
|
||||
.appFont(size: 16, weight: .bold)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
private func commentPreview(_ item: CommentedAudioResponse) -> some View {
|
||||
HStack(alignment: .center, spacing: SodaSpacing.s14) {
|
||||
DownsampledKFImage(
|
||||
url: URL(string: item.latestCommentWriterProfileImageUrl),
|
||||
size: CGSize(width: 28, height: 28)
|
||||
)
|
||||
.background(Color.gray700)
|
||||
.clipShape(Circle())
|
||||
|
||||
Text(item.latestComment)
|
||||
.appFont(size: 14, weight: .regular)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(2)
|
||||
.truncationMode(.tail)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 18, height: 18)
|
||||
}
|
||||
.padding(SodaSpacing.s12)
|
||||
.background(Color.gray800)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MainContentRecommendedAudioGridSection: View {
|
||||
let title: String
|
||||
let items: [AudioCardResponse]
|
||||
let onTapContent: (Int) -> Void
|
||||
|
||||
private let columns = [
|
||||
GridItem(.flexible(), spacing: SodaSpacing.s14),
|
||||
GridItem(.flexible(), spacing: SodaSpacing.s14)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
if !items.isEmpty {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
|
||||
SectionTitle(title: title)
|
||||
|
||||
LazyVGrid(columns: columns, alignment: .leading, spacing: SodaSpacing.s20) {
|
||||
ForEach(items) { item in
|
||||
Button {
|
||||
onTapContent(item.audioContentId)
|
||||
} label: {
|
||||
GeometryReader { proxy in
|
||||
let width = proxy.size.width
|
||||
|
||||
AudioContentThumbnailCard(
|
||||
item: AudioContentThumbnailCardItem(audioCard: item),
|
||||
width: width
|
||||
)
|
||||
}
|
||||
.aspectRatio(0.78, contentMode: .fit)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, SodaSpacing.s20)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MainContentVoiceOnOnlySection: View {
|
||||
let title: String
|
||||
let items: [OriginalSeriesResponse]
|
||||
let onTapSeries: (Int) -> Void
|
||||
|
||||
var body: some View {
|
||||
if !items.isEmpty {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
|
||||
SectionTitle(title: title)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .top, spacing: SodaSpacing.s14) {
|
||||
ForEach(items) { item in
|
||||
Button {
|
||||
onTapSeries(item.seriesId)
|
||||
} label: {
|
||||
ZStack(alignment: .topLeading) {
|
||||
DownsampledKFImage(
|
||||
url: URL(string: item.coverImageUrl),
|
||||
size: CGSize(width: 122, height: 172)
|
||||
)
|
||||
.background(Color.gray800)
|
||||
|
||||
originalTag
|
||||
}
|
||||
.frame(width: 122, height: 172)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, SodaSpacing.s20)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var originalTag: some View {
|
||||
HStack(spacing: SodaSpacing.s4) {
|
||||
Image("ic_series_original")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 14, height: 14)
|
||||
|
||||
Image("img_new_only")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(height: 14)
|
||||
}
|
||||
.padding(.horizontal, SodaSpacing.s8)
|
||||
.padding(.vertical, 5)
|
||||
.background(Color.gray900)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user