Files
sodalive-ios/SodaLive/Sources/Chat/Character/Detail/Gallery/CharacterDetailGalleryView.swift

204 lines
7.5 KiB
Swift

//
// CharacterDetailGalleryView.swift
// SodaLive
//
// Created by klaus on 9/1/25.
//
import SwiftUI
struct CharacterDetailGalleryView: View {
@StateObject var viewModel = CharacterDetailGalleryViewModel()
private let columns = Array(repeating: GridItem(.flexible(), spacing: 0), count: 3)
let characterId: Int
//
private var progressBarWidth: CGFloat {
let maxWidth: CGFloat = screenSize().width - 48
guard viewModel.totalCount > 0 else { return 0 }
let percentage = Double(viewModel.ownedCount) / Double(viewModel.totalCount)
return maxWidth * percentage
}
var body: some View {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 24) {
//
collectionInfoView()
.padding(.horizontal, 24)
//
ScrollView {
LazyVGrid(columns: columns, spacing: 0) {
ForEach(Array(viewModel.galleryItems.enumerated()), id: \.element.id) { index, item in
galleryImageView(item: item, index: index)
.onAppear {
viewModel.loadMoreIfNeeded(currentItem: item)
}
}
}
.frame(width: screenSize().width)
}
Spacer()
}
.padding(.top, 24)
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
GeometryReader { geo in
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(alignment: .center)
.frame(maxWidth: .infinity)
.padding(.horizontal, 33.3)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
}
.onAppear {
viewModel.characterId = characterId
viewModel.loadInitialData()
}
.sheet(isPresented: $viewModel.isShowImageViewer) {
ImageViewerView(
images: viewModel.ownedImages.map { $0.imageUrl },
selectedIndex: $viewModel.selectedImageIndex
)
}
.overlay {
if viewModel.isShowPurchaseDialog {
SodaDialog(
title: "구매 확인",
desc: "선택한 이미지를 구매하시겠습니까?",
confirmButtonTitle: viewModel.selectedItemPrice,
confirmButtonAction: viewModel.onPurchaseConfirm,
cancelButtonTitle: "취소",
cancelButtonAction: viewModel.onPurchaseCancel
)
}
}
}
@ViewBuilder
private func collectionInfoView() -> some View {
VStack(spacing: 8) {
// ( % , , )
HStack {
Text("\(viewModel.ownershipPercentage)% 보유중")
.font(.custom(Font.preBold.rawValue, size: 18))
.foregroundColor(.white)
Spacer()
HStack(spacing: 4) {
Text("\(viewModel.ownedCount)")
.font(.custom(Font.preRegular.rawValue, size: 16))
.foregroundColor(Color(hex: "#FDD453"))
Text("/")
.font(.custom(Font.preRegular.rawValue, size: 16))
.foregroundColor(.white)
Text("\(viewModel.totalCount)")
.font(.custom(Font.preRegular.rawValue, size: 16))
.foregroundColor(.white)
}
}
//
GeometryReader { geometry in
ZStack(alignment: .leading) {
//
RoundedRectangle(cornerRadius: 999)
.foregroundColor(Color(hex: "#37474F"))
.frame(height: 9)
// ( )
RoundedRectangle(cornerRadius: 999)
.fill(
LinearGradient(
colors: [Color(hex: "#80D8FF"), Color(hex: "#6D5ED7")],
startPoint: .leading,
endPoint: .trailing
)
)
.frame(width: min(progressBarWidth, geometry.size.width), height: 9)
}
}
.frame(height: 9)
}
}
@ViewBuilder
private func galleryImageView(item: CharacterImageListItemResponse, index: Int) -> some View {
ZStack {
//
AsyncImage(url: URL(string: item.imageUrl)) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Rectangle()
.fill(Color.gray.opacity(0.3))
}
.frame(width: 132, height: 165)
.clipped()
.cornerRadius(0)
//
if !item.isOwned {
//
Rectangle()
.fill(Color.black.opacity(0.2))
.frame(width: 132, height: 165)
//
VStack(spacing: 8) {
//
Image("ic_new_lock")
.resizable()
.scaledToFit()
.frame(width: 24)
//
HStack(spacing: 4) {
Image("ic_can")
.resizable()
.scaledToFit()
.frame(width: 16)
Text("\(item.imagePriceCan)")
.font(.custom(Font.preBold.rawValue, size: 16))
.foregroundColor(Color(hex: "#263238"))
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Color(hex: "#B5E7FA"))
.cornerRadius(30)
.overlay {
RoundedRectangle(cornerRadius: 30)
.strokeBorder(lineWidth: 1)
.foregroundColor(.button)
}
}
}
}
.onTapGesture {
viewModel.onImageTapped(item, index: index)
}
}
}
#Preview {
CharacterDetailGalleryView(characterId: 1)
}