feat(main-home): 추천 홈 공용 컴포넌트를 추가한다
This commit is contained in:
21
SodaLive/Resources/Assets.xcassets/v2/ic_new_community_lock.imageset/Contents.json
vendored
Normal file
21
SodaLive/Resources/Assets.xcassets/v2/ic_new_community_lock.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_new_community_lock.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
SodaLive/Resources/Assets.xcassets/v2/ic_new_community_lock.imageset/ic_new_community_lock.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/v2/ic_new_community_lock.imageset/ic_new_community_lock.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 427 B |
21
SodaLive/Resources/Assets.xcassets/v2/ic_new_follow.imageset/Contents.json
vendored
Normal file
21
SodaLive/Resources/Assets.xcassets/v2/ic_new_follow.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_new_follow.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
SodaLive/Resources/Assets.xcassets/v2/ic_new_follow.imageset/ic_new_follow.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/v2/ic_new_follow.imageset/ic_new_follow.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 386 B |
21
SodaLive/Resources/Assets.xcassets/v2/ic_new_following.imageset/Contents.json
vendored
Normal file
21
SodaLive/Resources/Assets.xcassets/v2/ic_new_following.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_new_following.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
SodaLive/Resources/Assets.xcassets/v2/ic_new_following.imageset/ic_new_following.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/v2/ic_new_following.imageset/ic_new_following.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 364 B |
58
SodaLive/Sources/V2/Component/Banner/BannerCarousel.swift
Normal file
58
SodaLive/Sources/V2/Component/Banner/BannerCarousel.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
import SwiftUI
|
||||
|
||||
struct BannerCarouselItem: Identifiable, Hashable {
|
||||
let id: String
|
||||
let imageUrl: String?
|
||||
|
||||
init(id: String, imageUrl: String?) {
|
||||
self.id = id
|
||||
self.imageUrl = imageUrl
|
||||
}
|
||||
}
|
||||
|
||||
struct BannerCarousel: View {
|
||||
let items: [BannerCarouselItem]
|
||||
let height: CGFloat
|
||||
let action: (BannerCarouselItem) -> Void
|
||||
|
||||
init(
|
||||
items: [BannerCarouselItem],
|
||||
height: CGFloat = 120,
|
||||
action: @escaping (BannerCarouselItem) -> Void = { _ in }
|
||||
) {
|
||||
self.items = items
|
||||
self.height = height
|
||||
self.action = action
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if !items.isEmpty {
|
||||
TabView {
|
||||
ForEach(items) { item in
|
||||
Button {
|
||||
action(item)
|
||||
} label: {
|
||||
DownsampledKFImage(
|
||||
url: URL(string: item.imageUrl ?? ""),
|
||||
size: CGSize(width: UIScreen.main.bounds.width - (SodaSpacing.s20 * 2), height: height)
|
||||
)
|
||||
.background(Color.gray800)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.horizontal, SodaSpacing.s20)
|
||||
}
|
||||
}
|
||||
.frame(height: height)
|
||||
.tabViewStyle(.page(indexDisplayMode: items.count > 1 ? .automatic : .never))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BannerCarousel_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
BannerCarousel(items: [BannerCarouselItem(id: "1", imageUrl: nil)])
|
||||
.background(Color.black)
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
}
|
||||
58
SodaLive/Sources/V2/Component/Button/FollowAllButton.swift
Normal file
58
SodaLive/Sources/V2/Component/Button/FollowAllButton.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
import SwiftUI
|
||||
|
||||
struct FollowAllButton: View {
|
||||
let isCompleted: Bool
|
||||
let isLoading: Bool
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
isCompleted: Bool,
|
||||
isLoading: Bool = false,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.isCompleted = isCompleted
|
||||
self.isLoading = isLoading
|
||||
self.action = action
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
if !isCompleted && !isLoading {
|
||||
action()
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: SodaSpacing.s6) {
|
||||
if isCompleted {
|
||||
Image("ic_new_following")
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 16, height: 16)
|
||||
}
|
||||
|
||||
Text(isCompleted ? I18n.HomeRecommendation.followAllCompleted : I18n.HomeRecommendation.followAll)
|
||||
.appFont(.body5)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(.horizontal, SodaSpacing.s16)
|
||||
.frame(height: 36)
|
||||
.background(isCompleted ? Color.gray700 : Color.button)
|
||||
.clipShape(Capsule())
|
||||
.opacity(isLoading ? 0.6 : 1)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(isCompleted || isLoading)
|
||||
}
|
||||
}
|
||||
|
||||
struct FollowAllButton_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack(spacing: SodaSpacing.s12) {
|
||||
FollowAllButton(isCompleted: false) {}
|
||||
FollowAllButton(isCompleted: true) {}
|
||||
}
|
||||
.padding(SodaSpacing.s20)
|
||||
.background(Color.black)
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
}
|
||||
66
SodaLive/Sources/V2/Component/Card/AiCharacterCard.swift
Normal file
66
SodaLive/Sources/V2/Component/Card/AiCharacterCard.swift
Normal file
@@ -0,0 +1,66 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AiCharacterCard: View {
|
||||
let name: String
|
||||
let description: String
|
||||
let profileImageUrl: String?
|
||||
let chatCount: Int?
|
||||
let originalTitle: String?
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: SodaSpacing.s12) {
|
||||
DownsampledKFImage(url: URL(string: profileImageUrl ?? ""), size: CGSize(width: 72, height: 72))
|
||||
.background(Color.gray800)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s4) {
|
||||
Text(name)
|
||||
.appFont(.heading4)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
|
||||
Text(description)
|
||||
.appFont(.body5)
|
||||
.foregroundColor(Color.gray500)
|
||||
.lineLimit(2)
|
||||
.truncationMode(.tail)
|
||||
|
||||
HStack(spacing: SodaSpacing.s8) {
|
||||
if let chatCount {
|
||||
Text("Chat \(chatCount)")
|
||||
.appFont(.caption2)
|
||||
.foregroundColor(Color.gray500)
|
||||
}
|
||||
|
||||
if let originalTitle, !originalTitle.isEmpty {
|
||||
Text(originalTitle)
|
||||
.appFont(.caption2)
|
||||
.foregroundColor(Color.gray500)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(SodaSpacing.s12)
|
||||
.background(Color.gray900)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||
}
|
||||
}
|
||||
|
||||
struct AiCharacterCard_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AiCharacterCard(
|
||||
name: "AI 캐릭터",
|
||||
description: "캐릭터 설명이 표시됩니다.",
|
||||
profileImageUrl: nil,
|
||||
chatCount: 128,
|
||||
originalTitle: "원작"
|
||||
)
|
||||
.padding(SodaSpacing.s20)
|
||||
.background(Color.black)
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
}
|
||||
130
SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift
Normal file
130
SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift
Normal file
@@ -0,0 +1,130 @@
|
||||
import SwiftUI
|
||||
|
||||
struct CommunityPostCard: View {
|
||||
let creatorNickname: String
|
||||
let creatorProfileImageUrl: String?
|
||||
let content: String
|
||||
let imageUrl: String?
|
||||
let price: Int?
|
||||
let existOrdered: Bool
|
||||
let createdAt: String?
|
||||
let likeCount: Int?
|
||||
let commentCount: Int?
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
|
||||
header
|
||||
|
||||
Text(content)
|
||||
.appFont(.body4)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(3)
|
||||
.truncationMode(.tail)
|
||||
|
||||
if hasImage {
|
||||
imageContent
|
||||
}
|
||||
|
||||
footer
|
||||
}
|
||||
.padding(SodaSpacing.s12)
|
||||
.background(Color.gray900)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
HStack(spacing: SodaSpacing.s8) {
|
||||
DownsampledKFImage(url: URL(string: creatorProfileImageUrl ?? ""), size: CGSize(width: 32, height: 32))
|
||||
.background(Color.gray800)
|
||||
.clipShape(Circle())
|
||||
|
||||
Text(creatorNickname)
|
||||
.appFont(.body5)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
}
|
||||
|
||||
private var imageContent: some View {
|
||||
ZStack {
|
||||
DownsampledKFImage(url: URL(string: imageUrl ?? ""), size: CGSize(width: 240, height: 150))
|
||||
.background(Color.gray800)
|
||||
.frame(maxWidth: .infinity)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s12, style: .continuous))
|
||||
.blur(radius: isLocked ? 8 : 0)
|
||||
|
||||
if isLocked {
|
||||
VStack(spacing: SodaSpacing.s6) {
|
||||
Image("ic_new_community_lock")
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 24, height: 24)
|
||||
|
||||
if let price {
|
||||
Text("\(price) can")
|
||||
.appFont(.caption2)
|
||||
.foregroundColor(Color.button)
|
||||
.padding(.horizontal, SodaSpacing.s12)
|
||||
.padding(.vertical, SodaSpacing.s6)
|
||||
.overlay(
|
||||
Capsule()
|
||||
.stroke(Color.button, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var footer: some View {
|
||||
HStack(spacing: SodaSpacing.s12) {
|
||||
if let createdAt, !createdAt.isEmpty {
|
||||
Text(createdAt)
|
||||
.appFont(.caption2)
|
||||
.foregroundColor(Color.gray500)
|
||||
}
|
||||
|
||||
if let likeCount {
|
||||
Text("Like \(likeCount)")
|
||||
.appFont(.caption2)
|
||||
.foregroundColor(Color.gray500)
|
||||
}
|
||||
|
||||
if let commentCount {
|
||||
Text("Comment \(commentCount)")
|
||||
.appFont(.caption2)
|
||||
.foregroundColor(Color.gray500)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var hasImage: Bool {
|
||||
!(imageUrl ?? "").isEmpty
|
||||
}
|
||||
|
||||
private var isLocked: Bool {
|
||||
(price ?? 0) > 0 && !existOrdered
|
||||
}
|
||||
}
|
||||
|
||||
struct CommunityPostCard_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CommunityPostCard(
|
||||
creatorNickname: "크리에이터",
|
||||
creatorProfileImageUrl: nil,
|
||||
content: "커뮤니티 본문입니다.",
|
||||
imageUrl: nil,
|
||||
price: nil,
|
||||
existOrdered: false,
|
||||
createdAt: "방금 전",
|
||||
likeCount: 10,
|
||||
commentCount: 2
|
||||
)
|
||||
.padding(SodaSpacing.s20)
|
||||
.background(Color.black)
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import SwiftUI
|
||||
|
||||
struct CreatorProfileGridItem: Identifiable, Hashable {
|
||||
let id: String
|
||||
let imageUrl: String?
|
||||
let name: String
|
||||
let subtitle: String?
|
||||
|
||||
init(
|
||||
id: String,
|
||||
imageUrl: String?,
|
||||
name: String,
|
||||
subtitle: String? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.imageUrl = imageUrl
|
||||
self.name = name
|
||||
self.subtitle = subtitle
|
||||
}
|
||||
}
|
||||
|
||||
struct CreatorProfileGrid: View {
|
||||
let items: [CreatorProfileGridItem]
|
||||
let columns: Int
|
||||
let spacing: CGFloat
|
||||
let action: (CreatorProfileGridItem) -> Void
|
||||
|
||||
init(
|
||||
items: [CreatorProfileGridItem],
|
||||
columns: Int = 3,
|
||||
spacing: CGFloat = SodaSpacing.s16,
|
||||
action: @escaping (CreatorProfileGridItem) -> Void = { _ in }
|
||||
) {
|
||||
self.items = items
|
||||
self.columns = columns
|
||||
self.spacing = spacing
|
||||
self.action = action
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
LazyVGrid(columns: gridColumns, alignment: .center, spacing: spacing) {
|
||||
ForEach(items) { item in
|
||||
CreatorProfileItem(
|
||||
imageUrl: item.imageUrl,
|
||||
name: item.name,
|
||||
subtitle: item.subtitle
|
||||
) {
|
||||
action(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var gridColumns: [GridItem] {
|
||||
Array(repeating: GridItem(.flexible(), spacing: spacing), count: columns)
|
||||
}
|
||||
}
|
||||
|
||||
struct CreatorProfileGrid_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CreatorProfileGrid(items: [
|
||||
CreatorProfileGridItem(id: "1", imageUrl: nil, name: "크리에이터 1"),
|
||||
CreatorProfileGridItem(id: "2", imageUrl: nil, name: "크리에이터 2"),
|
||||
CreatorProfileGridItem(id: "3", imageUrl: nil, name: "크리에이터 3")
|
||||
])
|
||||
.padding(SodaSpacing.s20)
|
||||
.background(Color.black)
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import SwiftUI
|
||||
|
||||
struct CreatorProfileItem: View {
|
||||
let imageUrl: String?
|
||||
let name: String
|
||||
let subtitle: String?
|
||||
let action: (() -> Void)?
|
||||
|
||||
init(
|
||||
imageUrl: String?,
|
||||
name: String,
|
||||
subtitle: String? = nil,
|
||||
action: (() -> Void)? = nil
|
||||
) {
|
||||
self.imageUrl = imageUrl
|
||||
self.name = name
|
||||
self.subtitle = subtitle
|
||||
self.action = action
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
action?()
|
||||
} label: {
|
||||
VStack(alignment: .center, spacing: SodaSpacing.s8) {
|
||||
profileImage
|
||||
|
||||
VStack(alignment: .center, spacing: 2) {
|
||||
Text(name)
|
||||
.appFont(.body4)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
|
||||
if let subtitle, !subtitle.isEmpty {
|
||||
Text(subtitle)
|
||||
.appFont(.caption2)
|
||||
.foregroundColor(Color.gray500)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(action == nil)
|
||||
}
|
||||
|
||||
private var profileImage: some View {
|
||||
DownsampledKFImage(url: URL(string: imageUrl ?? ""), size: CGSize(width: 72, height: 72))
|
||||
.background(Color.gray800)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
|
||||
struct CreatorProfileItem_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CreatorProfileItem(imageUrl: nil, name: "크리에이터", subtitle: "방금 활동")
|
||||
.frame(width: 96)
|
||||
.padding(SodaSpacing.s20)
|
||||
.background(Color.black)
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
}
|
||||
81
SodaLive/Sources/V2/Component/Text/ExpandableTextView.swift
Normal file
81
SodaLive/Sources/V2/Component/Text/ExpandableTextView.swift
Normal file
@@ -0,0 +1,81 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ExpandableTextView: View {
|
||||
let text: String
|
||||
let lineLimit: Int
|
||||
let moreTitle: String
|
||||
let collapseTitle: String
|
||||
let font: SodaTypography
|
||||
let foregroundColor: Color
|
||||
|
||||
@State private var isExpanded = false
|
||||
@State private var fullTextHeight: CGFloat = 0
|
||||
@State private var limitedTextHeight: CGFloat = 0
|
||||
|
||||
init(
|
||||
text: String,
|
||||
lineLimit: Int = 3,
|
||||
moreTitle: String = I18n.HomeRecommendation.more,
|
||||
collapseTitle: String = I18n.HomeRecommendation.collapse,
|
||||
font: SodaTypography = .body3,
|
||||
foregroundColor: Color = Color.gray500
|
||||
) {
|
||||
self.text = text
|
||||
self.lineLimit = lineLimit
|
||||
self.moreTitle = moreTitle
|
||||
self.collapseTitle = collapseTitle
|
||||
self.font = font
|
||||
self.foregroundColor = foregroundColor
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s8) {
|
||||
Text(text)
|
||||
.appFont(font)
|
||||
.foregroundColor(foregroundColor)
|
||||
.lineLimit(isExpanded ? nil : lineLimit)
|
||||
.background(measuringText(lineLimit: nil, height: $fullTextHeight))
|
||||
.background(measuringText(lineLimit: lineLimit, height: $limitedTextHeight))
|
||||
|
||||
if shouldShowToggle {
|
||||
Button {
|
||||
isExpanded.toggle()
|
||||
} label: {
|
||||
Text(isExpanded ? collapseTitle : moreTitle)
|
||||
.appFont(.body5)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var shouldShowToggle: Bool {
|
||||
fullTextHeight > limitedTextHeight + 1
|
||||
}
|
||||
|
||||
private func measuringText(lineLimit: Int?, height: Binding<CGFloat>) -> some View {
|
||||
Text(text)
|
||||
.appFont(font)
|
||||
.lineLimit(lineLimit)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.hidden()
|
||||
.background(
|
||||
GeometryReader { proxy in
|
||||
Color.clear
|
||||
.onAppear { height.wrappedValue = proxy.size.height }
|
||||
.onChange(of: proxy.size.height) { newHeight in height.wrappedValue = newHeight }
|
||||
.onChange(of: text) { _ in height.wrappedValue = proxy.size.height }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct ExpandableTextView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ExpandableTextView(text: "긴 사업자 정보 텍스트가 들어가는 영역입니다. 세 줄을 넘어가면 더보기 버튼이 표시되고, 더보기 버튼을 누르면 전체 문구가 표시됩니다.")
|
||||
.padding(SodaSpacing.s20)
|
||||
.background(Color.black)
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
}
|
||||
107
SodaLive/Sources/V2/Main/Home/MainHomeViewModel.swift
Normal file
107
SodaLive/Sources/V2/Main/Home/MainHomeViewModel.swift
Normal file
@@ -0,0 +1,107 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class MainHomeViewModel: ObservableObject {
|
||||
private let repository = MainHomeRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var isLoading = false
|
||||
@Published var isShowPopup = false
|
||||
@Published var errorMessage = ""
|
||||
@Published var recommendations: HomeRecommendationResponse?
|
||||
@Published private(set) var completedFollowKeys = Set<String>()
|
||||
@Published private(set) var followingKeys = Set<String>()
|
||||
|
||||
func fetchRecommendations() {
|
||||
isLoading = true
|
||||
|
||||
repository.getRecommendations()
|
||||
.sink { [weak self] result in
|
||||
guard let self else { return }
|
||||
|
||||
switch result {
|
||||
case .finished:
|
||||
DEBUG_LOG("finish")
|
||||
case .failure(let error):
|
||||
ERROR_LOG(error.localizedDescription)
|
||||
self.errorMessage = I18n.Common.commonError
|
||||
self.isShowPopup = true
|
||||
self.isLoading = false
|
||||
}
|
||||
} receiveValue: { [weak self] response in
|
||||
guard let self else { return }
|
||||
|
||||
let responseData = response.data
|
||||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<HomeRecommendationResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.recommendations = data
|
||||
} else {
|
||||
self.errorMessage = decoded.message ?? I18n.Common.commonError
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = I18n.Common.commonError
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func followAll(creatorIds: [Int], completionKey: String) {
|
||||
guard !creatorIds.isEmpty, !completionKey.isEmpty else { return }
|
||||
guard !completedFollowKeys.contains(completionKey), !followingKeys.contains(completionKey) else { return }
|
||||
|
||||
followingKeys.insert(completionKey)
|
||||
|
||||
repository.followRecommendedCreators(request: FollowRecommendedCreatorsRequest(creatorIds: creatorIds))
|
||||
.sink { [weak self] result in
|
||||
guard let self else { return }
|
||||
|
||||
switch result {
|
||||
case .finished:
|
||||
DEBUG_LOG("finish")
|
||||
case .failure(let error):
|
||||
ERROR_LOG(error.localizedDescription)
|
||||
self.errorMessage = I18n.Common.commonError
|
||||
self.isShowPopup = true
|
||||
self.followingKeys.remove(completionKey)
|
||||
}
|
||||
} receiveValue: { [weak self] response in
|
||||
guard let self else { return }
|
||||
|
||||
let responseData = response.data
|
||||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
|
||||
|
||||
if decoded.success {
|
||||
self.completedFollowKeys.insert(completionKey)
|
||||
} else {
|
||||
self.errorMessage = decoded.message ?? I18n.Common.commonError
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = I18n.Common.commonError
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.followingKeys.remove(completionKey)
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func isFollowAllCompleted(_ completionKey: String) -> Bool {
|
||||
completedFollowKeys.contains(completionKey)
|
||||
}
|
||||
|
||||
func isFollowAllLoading(_ completionKey: String) -> Bool {
|
||||
followingKeys.contains(completionKey)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user