feat: 视频列表增加 checkbox 选择列和全选按钮,修复 PlayInfo 解码失败导致下载面板无选项的问题
CI / lint-and-build (push) Has been cancelled
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:
@@ -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,6 +37,10 @@ struct DownloadPanelView: View {
|
|||||||
Text(item.video.title)
|
Text(item.video.title)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
Spacer()
|
Spacer()
|
||||||
|
if item.playInfos.isEmpty {
|
||||||
|
Text("无可用播放地址")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
} else {
|
||||||
Picker("清晰度", selection: Binding(
|
Picker("清晰度", selection: Binding(
|
||||||
get: { selectedDefinitions[item.video.videoId] ?? item.playInfos.first?.definition ?? "" },
|
get: { selectedDefinitions[item.video.videoId] ?? item.playInfos.first?.definition ?? "" },
|
||||||
set: { selectedDefinitions[item.video.videoId] = $0 }
|
set: { selectedDefinitions[item.video.videoId] = $0 }
|
||||||
@@ -49,6 +53,7 @@ struct DownloadPanelView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let error = errorMessage {
|
if let error = errorMessage {
|
||||||
HStack {
|
HStack {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user