feat: 视频列表增加 checkbox 选择列和全选按钮,修复 PlayInfo 解码失败导致下载面板无选项的问题
CI / lint-and-build (push) Has been cancelled

- PlayInfo 添加 CodingKeys 映射 PascalCase JSON 字段(PlayURL/Definition/Duration/Size/Format)\n- Table 第一列增加圆形 checkbox 图标,点击可切换选中状态\n- 增加全选/取消全选按钮\n- prepareDownload 增加错误提示\n- 下载面板无播放地址时显示提示文字
This commit is contained in:
2026-06-30 09:23:16 +06:00
parent fe6f2bbda1
commit a6206b6881
4 changed files with 52 additions and 7 deletions
+2
View File
@@ -14,3 +14,5 @@ let package = Package(
), ),
] ]
) )
@@ -131,6 +131,14 @@ struct PlayInfo: Codable, Equatable {
var duration: String? var duration: String?
var size: Int64? var size: Int64?
var format: String? var format: String?
enum CodingKeys: String, CodingKey {
case playURL = "PlayURL"
case definition = "Definition"
case duration = "Duration"
case size = "Size"
case format = "Format"
}
} }
struct CategoryItem: Identifiable, Codable, Equatable { struct CategoryItem: Identifiable, Codable, Equatable {
@@ -37,15 +37,20 @@ struct DownloadPanelView: View {
Text(item.video.title) Text(item.video.title)
.lineLimit(1) .lineLimit(1)
Spacer() Spacer()
Picker("清晰度", selection: Binding( if item.playInfos.isEmpty {
get: { selectedDefinitions[item.video.videoId] ?? item.playInfos.first?.definition ?? "" }, Text("无可用播放地址")
set: { selectedDefinitions[item.video.videoId] = $0 } .foregroundStyle(.secondary)
)) { } else {
ForEach(item.playInfos, id: \.definition) { info in Picker("清晰度", selection: Binding(
Text("\(info.definition) - \(formatSize(info.size))").tag(info.definition) get: { selectedDefinitions[item.video.videoId] ?? item.playInfos.first?.definition ?? "" },
set: { selectedDefinitions[item.video.videoId] = $0 }
)) {
ForEach(item.playInfos, id: \.definition) { info in
Text("\(info.definition) - \(formatSize(info.size))").tag(info.definition)
}
} }
.frame(width: 200)
} }
.frame(width: 200)
} }
} }
} }
@@ -38,6 +38,14 @@ struct VideoListView: View {
} }
.disabled(selectedVideos.isEmpty || isLoading) .disabled(selectedVideos.isEmpty || isLoading)
Button("全选") {
if selectedVideos.count == videos.count {
selectedVideos.removeAll()
} else {
selectedVideos = Set(videos.map(\.videoId))
}
}
Button("批量删除") { Button("批量删除") {
Task { await batchDelete() } Task { await batchDelete() }
} }
@@ -65,6 +73,19 @@ struct VideoListView: View {
} }
Table(of: VideoItem.self, selection: $selectedVideos) { Table(of: VideoItem.self, selection: $selectedVideos) {
TableColumn("") { video in
Image(systemName: selectedVideos.contains(video.videoId) ? "checkmark.circle.fill" : "circle")
.foregroundStyle(selectedVideos.contains(video.videoId) ? .blue : .secondary)
.onTapGesture {
if selectedVideos.contains(video.videoId) {
selectedVideos.remove(video.videoId)
} else {
selectedVideos.insert(video.videoId)
}
}
}
.width(min: 30, ideal: 30, max: 30)
TableColumn("标题") { video in TableColumn("标题") { video in
HStack { HStack {
AsyncImage(url: video.coverURL.flatMap(URL.init)) { image in AsyncImage(url: video.coverURL.flatMap(URL.init)) { image in
@@ -172,15 +193,24 @@ struct VideoListView: View {
private func prepareDownload() async { private func prepareDownload() async {
isLoading = true isLoading = true
errorMessage = nil
var results: [(video: VideoItem, playInfos: [PlayInfo])] = [] var results: [(video: VideoItem, playInfos: [PlayInfo])] = []
var errors: [String] = []
for video in videos where selectedVideos.contains(video.videoId) { for video in videos where selectedVideos.contains(video.videoId) {
do { do {
let response = try await VODClient.shared.getPlayInfo(videoId: video.videoId) let response = try await VODClient.shared.getPlayInfo(videoId: video.videoId)
results.append((video, response.playInfos)) results.append((video, response.playInfos))
if response.playInfos.isEmpty {
errors.append("\(video.title)」无可用播放地址")
}
} catch { } catch {
results.append((video, [])) results.append((video, []))
errors.append("\(video.title)」获取播放信息失败:\(error.localizedDescription)")
} }
} }
if !errors.isEmpty {
errorMessage = errors.joined(separator: "\n")
}
downloadResults = results downloadResults = results
showDownloadPanel = true showDownloadPanel = true
isLoading = false isLoading = false