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() } } } }