313 lines
11 KiB
Swift
313 lines
11 KiB
Swift
//
|
|
// ListView.swift
|
|
// dimensionhub
|
|
//
|
|
// Created by 安礼成 on 2025/2/21.
|
|
//
|
|
import SwiftUI
|
|
import Observation
|
|
|
|
@Observable
|
|
final class DetailModel {
|
|
|
|
struct Episode: Codable, Identifiable {
|
|
let id = UUID().uuidString
|
|
let name: String
|
|
let num_name: String
|
|
let thumb: String
|
|
let play: String
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case name, num_name, thumb, play
|
|
}
|
|
}
|
|
|
|
// 渠道
|
|
struct Channel: Codable {
|
|
let name: String
|
|
let episodes: [Episode]
|
|
}
|
|
|
|
struct DramaDetailResponse: Codable {
|
|
let name: String
|
|
let summary: String
|
|
let thumb: String
|
|
let status: [String]
|
|
let channels: [Channel]
|
|
}
|
|
|
|
// 状态信息
|
|
struct DramaStatus {
|
|
struct DisplayConfig {
|
|
let name: String
|
|
let bgColor: Color
|
|
let fontColor: Color
|
|
}
|
|
|
|
let status: String
|
|
let config: DisplayConfig
|
|
|
|
init?(_ rawValue: String) {
|
|
switch rawValue {
|
|
case "first_row":
|
|
self.status = rawValue
|
|
self.config = DisplayConfig(name: "前排占位", bgColor: .black, fontColor: .white)
|
|
case "following":
|
|
self.status = rawValue
|
|
self.config = DisplayConfig(name: "追番", bgColor: .black, fontColor: .white)
|
|
case "catching_up":
|
|
self.status = rawValue
|
|
self.config = DisplayConfig(name: "补番", bgColor: .black, fontColor: .white)
|
|
case "dropping":
|
|
self.status = rawValue
|
|
self.config = DisplayConfig(name: "弃番", bgColor: .white, fontColor: Color(hex: "#333333"))
|
|
case "finished":
|
|
self.status = rawValue
|
|
self.config = DisplayConfig(name: "补完", bgColor: .black, fontColor: .white)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
enum FollowResult {
|
|
case success
|
|
case error(String, String)
|
|
}
|
|
|
|
var name: String = ""
|
|
var summary: String = ""
|
|
var thumb: String = ""
|
|
var statuses: [DramaStatus] = []
|
|
var channels: [Channel] = []
|
|
|
|
// 当前选中的channel
|
|
var selectedChannelIdx: Int = 0
|
|
var selectedEpisodes: [Episode] = []
|
|
|
|
@MainActor
|
|
func loadData(userId: String, id: Int) async {
|
|
let response = await API.getDramaDetail(userId: userId, id: id, as: DramaDetailResponse.self)
|
|
switch response {
|
|
case .error(let code, let message):
|
|
print(code)
|
|
print(message)
|
|
case .result(let detail):
|
|
self.name = detail.name
|
|
self.summary = Utils.converHtmlToString(html: detail.summary) ?? ""
|
|
self.thumb = detail.thumb
|
|
self.statuses = detail.status.flatMap({ s in
|
|
if let status = DramaStatus(s) {
|
|
return [status]
|
|
} else {
|
|
return []
|
|
}
|
|
})
|
|
self.channels = detail.channels
|
|
|
|
self.selectedChannelIdx = 0
|
|
self.selectedEpisodes = detail.channels[0].episodes
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
func toggleChannel(channelIdx: Int) {
|
|
self.selectedChannelIdx = channelIdx
|
|
self.selectedEpisodes = self.channels[channelIdx].episodes
|
|
}
|
|
|
|
@MainActor
|
|
func onTapFollowButton(userId: String, id: Int, status: String) async -> FollowResult {
|
|
let response = await API.followDrama(userId: userId, id: id, status: status, as: [String].self)
|
|
switch response {
|
|
case .error(_, let message):
|
|
return .error("错误", message)
|
|
case .result(let newStatuses):
|
|
self.statuses = newStatuses.flatMap({ s in
|
|
if let status = DramaStatus(s) {
|
|
return [status]
|
|
} else {
|
|
return []
|
|
}
|
|
})
|
|
return .success
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
struct DetailView: View {
|
|
@AppStorage("userId") private var userId: String = Utils.defaultUserId()
|
|
|
|
@State var detailModel = DetailModel()
|
|
@State var showAllSummary: Bool = false
|
|
|
|
// 错误提示信息
|
|
@State var showAlert: Bool = false
|
|
@State var errorInfo: (String, String) = ("", "")
|
|
|
|
let id: Int
|
|
|
|
var body: some View {
|
|
VStack(alignment: .center) {
|
|
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
HStack {
|
|
Text(detailModel.name)
|
|
.font(.system(size: 28))
|
|
.fontWeight(.bold)
|
|
.foregroundColor(Color(hex: "#333333"))
|
|
Spacer()
|
|
}
|
|
if showAllSummary {
|
|
Text(detailModel.summary)
|
|
.lineLimit(nil)
|
|
.multilineTextAlignment(.leading)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.onTapGesture {
|
|
withAnimation {
|
|
self.showAllSummary = false
|
|
}
|
|
}
|
|
|
|
} else {
|
|
Text(detailModel.summary)
|
|
.lineLimit(3)
|
|
.multilineTextAlignment(.leading)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.onTapGesture {
|
|
withAnimation {
|
|
self.showAllSummary = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding([.top, .leading], 10)
|
|
.background(Color(hex: "#F2F2F2"), ignoresSafeAreaEdges: [.bottom])
|
|
|
|
HStack(alignment: .center, spacing: 10) {
|
|
Spacer()
|
|
ForEach(detailModel.statuses, id: \.status) { status in
|
|
FollowButtonView(dramaStatus: status) { followStatus in
|
|
Task {
|
|
let result = await detailModel.onTapFollowButton(userId: self.userId, id: id, status: followStatus)
|
|
switch result {
|
|
case .success:
|
|
()
|
|
case .error(let title, let message):
|
|
self.errorInfo = (title, message)
|
|
self.showAlert = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 渠道列表
|
|
HStack(alignment: .center, spacing: 15) {
|
|
ForEach(Array(detailModel.channels.enumerated()), id: \.offset) { idx, channel in
|
|
Text(channel.name)
|
|
.font(.system(size: 13))
|
|
.foregroundColor(idx == detailModel.selectedChannelIdx ? Color(hex: "#169BD5") : Color(hex: "#666666"))
|
|
.onTapGesture {
|
|
detailModel.toggleChannel(channelIdx: idx)
|
|
}
|
|
}
|
|
Spacer()
|
|
}
|
|
.padding(.leading, 10)
|
|
|
|
// 渠道相关的数据列表
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(alignment: .center) {
|
|
ForEach(detailModel.selectedEpisodes) { episode in
|
|
VStack(alignment: .center) {
|
|
AsyncImage(url: URL(string: episode.thumb)) { image in
|
|
image.resizable()
|
|
} placeholder: {
|
|
ProgressView()
|
|
}
|
|
.frame(width: 90, height: 70)
|
|
.overlay {
|
|
VStack {
|
|
HStack {
|
|
Text(episode.num_name)
|
|
.font(.system(size: 12))
|
|
.foregroundColor(Color(hex: "#333333"))
|
|
Spacer()
|
|
}
|
|
Spacer()
|
|
}
|
|
.padding([.top, .leading], 8)
|
|
}
|
|
|
|
Text(episode.name)
|
|
.font(.system(size: 12))
|
|
.foregroundColor(Color(hex: "#333333"))
|
|
.lineLimit(1)
|
|
}
|
|
.frame(width: 90, height: 120)
|
|
}
|
|
}
|
|
}
|
|
|
|
HStack(alignment: .center) {
|
|
NavigationLink(destination: ListView(id: self.id)) {
|
|
Rectangle()
|
|
.frame(width: 200, height: 25)
|
|
.foregroundColor(Color(hex: "#F2F2F2"))
|
|
.overlay {
|
|
Text("展开全部剧集")
|
|
.font(.system(size: 13))
|
|
.foregroundColor(Color(hex: "#999999"))
|
|
.fontWeight(.regular)
|
|
}
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
.frame(width: 370, alignment: .center)
|
|
.alert(isPresented: $showAlert) {
|
|
Alert(title: Text(self.errorInfo.0), message: Text(self.errorInfo.1), dismissButton: .default(Text("OK")))
|
|
}
|
|
.task {
|
|
await detailModel.loadData(userId: self.userId, id: self.id)
|
|
print(detailModel.summary)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension DetailView {
|
|
|
|
struct FollowButtonView: View {
|
|
let dramaStatus: DetailModel.DramaStatus
|
|
let onTap: (String) -> Void
|
|
|
|
var body: some View {
|
|
Rectangle()
|
|
.frame(width: 140, height: 40)
|
|
.foregroundColor(dramaStatus.config.bgColor)
|
|
.cornerRadius(5)
|
|
.overlay {
|
|
RoundedRectangle(cornerRadius: 5)
|
|
.stroke(Color.black, lineWidth: 1)
|
|
|
|
Text(dramaStatus.config.name)
|
|
.font(.system(size: 13))
|
|
.foregroundColor(dramaStatus.config.fontColor)
|
|
.fontWeight(.regular)
|
|
}
|
|
.onTapGesture {
|
|
onTap(dramaStatus.status)
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
#Preview {
|
|
DetailView(id: 19625)
|
|
}
|