129 lines
4.2 KiB
Swift
129 lines
4.2 KiB
Swift
import SwiftUI
|
|
|
|
struct ContentView: View {
|
|
@StateObject private var vm = DownloadViewModel()
|
|
|
|
var body: some View {
|
|
VStack(spacing: 16) {
|
|
header
|
|
|
|
GroupBox("Download") {
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
HStack {
|
|
TextField("YouTube video or playlist URL", text: $vm.urlString)
|
|
.textFieldStyle(.roundedBorder)
|
|
|
|
Button("Paste") {
|
|
if let s = NSPasteboard.general.string(forType: .string) {
|
|
vm.urlString = s.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
}
|
|
}
|
|
|
|
HStack(spacing: 16) {
|
|
Toggle("Audio only (MP3)", isOn: $vm.audioOnly)
|
|
|
|
Picker("Quality", selection: $vm.quality) {
|
|
ForEach(VideoQuality.allCases, id: \.self) { q in
|
|
Text(q.title).tag(q)
|
|
}
|
|
}
|
|
.pickerStyle(.menu)
|
|
|
|
Spacer()
|
|
}
|
|
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("Save to:")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
|
|
Text(vm.destinationPathDisplay)
|
|
.lineLimit(1)
|
|
.truncationMode(.middle)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Button("Choose Folder…") { vm.pickFolder() }
|
|
}
|
|
|
|
Divider()
|
|
|
|
HStack(spacing: 12) {
|
|
Button(vm.isDownloading ? "Downloading…" : "Start") {
|
|
vm.start()
|
|
}
|
|
.disabled(!vm.canStart)
|
|
|
|
Button("Cancel") {
|
|
vm.cancel()
|
|
}
|
|
.disabled(!vm.isDownloading)
|
|
|
|
Spacer()
|
|
}
|
|
}
|
|
.padding(8)
|
|
}
|
|
|
|
GroupBox("Progress") {
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
ProgressView(value: vm.progress)
|
|
.progressViewStyle(.linear)
|
|
|
|
HStack {
|
|
Text(vm.statusLine)
|
|
.font(.system(.caption, design: .monospaced))
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(2)
|
|
|
|
Spacer()
|
|
|
|
if vm.isDownloading {
|
|
Text(vm.percentText)
|
|
.font(.system(.caption, design: .monospaced))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
.padding(8)
|
|
}
|
|
|
|
GroupBox("Log") {
|
|
ScrollView {
|
|
Text(vm.log)
|
|
.font(.system(.caption, design: .monospaced))
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.textSelection(.enabled)
|
|
.padding(.vertical, 6)
|
|
}
|
|
.frame(height: 140)
|
|
.padding(.horizontal, 8)
|
|
}
|
|
}
|
|
.padding(18)
|
|
.alert("Error", isPresented: $vm.showError) {
|
|
Button("OK", role: .cancel) { }
|
|
} message: {
|
|
Text(vm.errorMessage)
|
|
}
|
|
}
|
|
|
|
private var header: some View {
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("YoutubeDumper")
|
|
.font(.title2.bold())
|
|
Text("Uses Homebrew yt-dlp. Supports videos, playlists, and audio-only.")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
Spacer()
|
|
Button("Check yt-dlp") {
|
|
vm.checkYtDlp()
|
|
}
|
|
}
|
|
}
|
|
}
|