Merge branch 'main' into feature_tvOS
This commit is contained in:
@@ -93,10 +93,6 @@ enum HTMLParser {
|
|||||||
let h1 = try doc.select("h1").first()
|
let h1 = try doc.select("h1").first()
|
||||||
let fullTitle = try h1?.text().trimmingCharacters(in: .whitespacesAndNewlines) ?? "未知标题"
|
let fullTitle = try h1?.text().trimmingCharacters(in: .whitespacesAndNewlines) ?? "未知标题"
|
||||||
|
|
||||||
// 海报
|
|
||||||
let imgSrc = try doc.select("img.w-full.h-full.object-cover").first()?.attr("src") ?? ""
|
|
||||||
let posterURL = URL(string: imgSrc)
|
|
||||||
|
|
||||||
// === 优先从 JSON-LD 提取结构化数据 ===
|
// === 优先从 JSON-LD 提取结构化数据 ===
|
||||||
var year = 0
|
var year = 0
|
||||||
var rating: Double?
|
var rating: Double?
|
||||||
@@ -105,12 +101,17 @@ enum HTMLParser {
|
|||||||
var genres: [String] = []
|
var genres: [String] = []
|
||||||
var description = ""
|
var description = ""
|
||||||
var region = ""
|
var region = ""
|
||||||
|
var posterURL: URL?
|
||||||
|
|
||||||
if let jsonLDScript = try doc.select("script[type=application/ld+json]").first() {
|
if let jsonLDScript = try doc.select("script[type=application/ld+json]").first() {
|
||||||
let jsonText = try jsonLDScript.data()
|
let jsonText = try jsonLDScript.data()
|
||||||
if let data = jsonText.data(using: .utf8),
|
if let data = jsonText.data(using: .utf8),
|
||||||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
||||||
|
|
||||||
|
if let imageStr = json["image"] as? String {
|
||||||
|
posterURL = URL(string: imageStr)
|
||||||
|
}
|
||||||
|
|
||||||
if let published = json["datePublished"] as? String {
|
if let published = json["datePublished"] as? String {
|
||||||
year = Int(published) ?? 0
|
year = Int(published) ?? 0
|
||||||
}
|
}
|
||||||
@@ -140,6 +141,12 @@ enum HTMLParser {
|
|||||||
|
|
||||||
// === 从 HTML 补充缺失数据 ===
|
// === 从 HTML 补充缺失数据 ===
|
||||||
|
|
||||||
|
// 海报 fallback: 主海报在 .flex-shrink-0 容器中
|
||||||
|
if posterURL == nil {
|
||||||
|
let imgSrc = try doc.select(".flex-shrink-0 img.object-cover").first()?.attr("src") ?? ""
|
||||||
|
posterURL = URL(string: imgSrc)
|
||||||
|
}
|
||||||
|
|
||||||
// 评分 fallback
|
// 评分 fallback
|
||||||
if rating == nil {
|
if rating == nil {
|
||||||
let ratingText = try doc.select(".rating-display").text()
|
let ratingText = try doc.select(".rating-display").text()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@Observable
|
@Observable @MainActor
|
||||||
final class DetailViewModel {
|
final class DetailViewModel {
|
||||||
var detail: ContentDetail?
|
var detail: ContentDetail?
|
||||||
var selectedSourceIndex = 0
|
var selectedSourceIndex = 0
|
||||||
@@ -27,23 +27,22 @@ final class DetailViewModel {
|
|||||||
func loadDetail(path: String) async {
|
func loadDetail(path: String) async {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
error = nil
|
error = nil
|
||||||
defer { isLoading = false }
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let html = try await APIClient.shared.fetchDetailPage(path: path)
|
let html = try await APIClient.shared.fetchDetailPage(path: path)
|
||||||
let parsedDetail = try HTMLParser.parseContentDetail(html: html)
|
let parsedDetail = try HTMLParser.parseContentDetail(html: html)
|
||||||
await MainActor.run {
|
|
||||||
self.detail = parsedDetail
|
self.detail = parsedDetail
|
||||||
self.selectedSourceIndex = 0
|
self.selectedSourceIndex = 0
|
||||||
self.selectedEpisodeIndex = 0
|
self.selectedEpisodeIndex = 0
|
||||||
}
|
|
||||||
} catch is CancellationError {
|
} catch is CancellationError {
|
||||||
|
// 被取消时不清除 loading 状态,让视图继续显示加载中
|
||||||
return
|
return
|
||||||
} catch let error as URLError where error.code == .cancelled {
|
} catch let error as URLError where error.code == .cancelled {
|
||||||
return
|
return
|
||||||
} catch {
|
} catch {
|
||||||
await MainActor.run { self.error = error.localizedDescription }
|
self.error = error.localizedDescription
|
||||||
}
|
}
|
||||||
|
isLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func selectSource(_ index: Int) {
|
func selectSource(_ index: Int) {
|
||||||
|
|||||||
@@ -16,22 +16,22 @@ struct DetailView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if viewModel.isLoading {
|
if let detail = viewModel.detail {
|
||||||
ProgressView()
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
} else if let error = viewModel.error {
|
|
||||||
errorView(error)
|
|
||||||
} else if let detail = viewModel.detail {
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
detailContent(detail)
|
detailContent(detail)
|
||||||
}
|
}
|
||||||
|
} else if let error = viewModel.error {
|
||||||
|
errorView(error)
|
||||||
|
} else {
|
||||||
|
ProgressView()
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(item.title)
|
.navigationTitle(item.title)
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
#endif
|
#endif
|
||||||
.task {
|
.task(id: item.id) {
|
||||||
if viewModel.detail == nil {
|
if viewModel.detail == nil {
|
||||||
await viewModel.loadDetail(path: item.detailURL)
|
await viewModel.loadDetail(path: item.detailURL)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user