208 lines
6.7 KiB
Swift
208 lines
6.7 KiB
Swift
import SwiftUI
|
|
|
|
struct VideoListView: View {
|
|
@State private var videos: [VideoItem] = []
|
|
@State private var selectedVideos: Set<String> = []
|
|
@State private var keyword: String = ""
|
|
@State private var isLoading = false
|
|
@State private var total = 0
|
|
@State private var pageNo = 1
|
|
@State private var pageSize = 10
|
|
@State private var errorMessage: String?
|
|
@State private var showDownloadPanel = false
|
|
@State private var downloadResults: [(video: VideoItem, playInfos: [PlayInfo])] = []
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
HStack(spacing: 12) {
|
|
TextField("搜索标题 / 视频 ID", text: $keyword)
|
|
.textFieldStyle(.roundedBorder)
|
|
.frame(minWidth: 200)
|
|
|
|
Button("搜索") {
|
|
pageNo = 1
|
|
Task { await fetch() }
|
|
}
|
|
.keyboardShortcut(.return, modifiers: [.command])
|
|
|
|
Button("重置") {
|
|
keyword = ""
|
|
pageNo = 1
|
|
Task { await fetch() }
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Button("批量下载") {
|
|
Task { await prepareDownload() }
|
|
}
|
|
.disabled(selectedVideos.isEmpty || isLoading)
|
|
|
|
Button("批量删除") {
|
|
Task { await batchDelete() }
|
|
}
|
|
.disabled(selectedVideos.isEmpty)
|
|
.buttonStyle(.borderedProminent)
|
|
.tint(.red)
|
|
}
|
|
.padding()
|
|
|
|
if let error = errorMessage {
|
|
HStack {
|
|
Text(error)
|
|
.foregroundStyle(.red)
|
|
Button {
|
|
NSPasteboard.general.clearContents()
|
|
NSPasteboard.general.setString(error, forType: .string)
|
|
} label: {
|
|
Image(systemName: "doc.on.doc")
|
|
}
|
|
.buttonStyle(.borderless)
|
|
.help("复制错误信息")
|
|
Spacer()
|
|
}
|
|
.padding(.horizontal)
|
|
}
|
|
|
|
Table(of: VideoItem.self, selection: $selectedVideos) {
|
|
TableColumn("标题") { video in
|
|
HStack {
|
|
AsyncImage(url: video.coverURL.flatMap(URL.init)) { image in
|
|
image.resizable().scaledToFill()
|
|
} placeholder: {
|
|
Color.gray
|
|
}
|
|
.frame(width: 60, height: 40)
|
|
.cornerRadius(4)
|
|
|
|
Text(video.title)
|
|
.lineLimit(2)
|
|
}
|
|
}
|
|
.width(min: 200, ideal: 300)
|
|
|
|
TableColumn("视频 ID") { video in
|
|
Text(video.videoId)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.width(min: 120, ideal: 180)
|
|
|
|
TableColumn("时长") { video in
|
|
Text(formatDuration(video.duration))
|
|
}
|
|
.width(min: 60, ideal: 80)
|
|
|
|
TableColumn("大小") { video in
|
|
Text(formatSize(video.size))
|
|
}
|
|
.width(min: 80, ideal: 100)
|
|
|
|
TableColumn("状态") { video in
|
|
Text(video.status ?? "-")
|
|
}
|
|
.width(min: 60, ideal: 80)
|
|
|
|
TableColumn("创建时间") { video in
|
|
Text(video.creationTime ?? "-")
|
|
}
|
|
.width(min: 120, ideal: 160)
|
|
} rows: {
|
|
ForEach(videos) { video in
|
|
TableRow(video)
|
|
}
|
|
}
|
|
.frame(minHeight: 300)
|
|
|
|
HStack {
|
|
Text("共 \(total) 条,已选 \(selectedVideos.count) 个")
|
|
.foregroundStyle(.secondary)
|
|
Spacer()
|
|
Button("上一页") {
|
|
pageNo -= 1
|
|
Task { await fetch() }
|
|
}
|
|
.disabled(pageNo <= 1)
|
|
Text("第 \(pageNo) 页")
|
|
Button("下一页") {
|
|
pageNo += 1
|
|
Task { await fetch() }
|
|
}
|
|
.disabled(pageNo * pageSize >= total)
|
|
}
|
|
.padding()
|
|
}
|
|
.task {
|
|
await fetch()
|
|
}
|
|
.sheet(isPresented: $showDownloadPanel) {
|
|
DownloadPanelView(items: downloadResults)
|
|
}
|
|
}
|
|
|
|
private func fetch() async {
|
|
isLoading = true
|
|
errorMessage = nil
|
|
do {
|
|
let response = try await VODClient.shared.searchMedia(
|
|
keyword: keyword.isEmpty ? nil : keyword,
|
|
pageNo: pageNo,
|
|
pageSize: pageSize
|
|
)
|
|
videos = response.videos
|
|
total = response.Total ?? 0
|
|
pageNo = response.PageNo ?? pageNo
|
|
pageSize = response.PageSize ?? pageSize
|
|
} catch {
|
|
errorMessage = "加载失败:\(error.localizedDescription)"
|
|
}
|
|
isLoading = false
|
|
}
|
|
|
|
private func batchDelete() async {
|
|
let ids = Array(selectedVideos)
|
|
do {
|
|
try await VODClient.shared.deleteVideos(videoIds: ids)
|
|
selectedVideos.removeAll()
|
|
await fetch()
|
|
} catch {
|
|
errorMessage = "删除失败:\(error.localizedDescription)"
|
|
}
|
|
}
|
|
|
|
private func prepareDownload() async {
|
|
isLoading = true
|
|
var results: [(video: VideoItem, playInfos: [PlayInfo])] = []
|
|
for video in videos where selectedVideos.contains(video.videoId) {
|
|
do {
|
|
let response = try await VODClient.shared.getPlayInfo(videoId: video.videoId)
|
|
results.append((video, response.playInfos))
|
|
} catch {
|
|
results.append((video, []))
|
|
}
|
|
}
|
|
downloadResults = results
|
|
showDownloadPanel = true
|
|
isLoading = false
|
|
}
|
|
|
|
private func formatDuration(_ seconds: Double?) -> String {
|
|
guard let seconds = seconds else { return "-" }
|
|
let m = Int(seconds) / 60
|
|
let s = Int(seconds) % 60
|
|
return "\(m):\(String(format: "%02d", s))"
|
|
}
|
|
|
|
private func formatSize(_ bytes: Int64?) -> String {
|
|
guard let bytes = bytes else { return "-" }
|
|
let units = ["B", "KB", "MB", "GB", "TB"]
|
|
var size = Double(bytes)
|
|
var i = 0
|
|
while size >= 1024 && i < units.count - 1 {
|
|
size /= 1024
|
|
i += 1
|
|
}
|
|
return String(format: "%.2f %@", size, units[i])
|
|
}
|
|
}
|