first commit

This commit is contained in:
Victor Fedorov
2026-01-20 19:35:38 +07:00
commit fd180ccd2e
10 changed files with 669 additions and 0 deletions

128
ContentView.swift Normal file
View File

@@ -0,0 +1,128 @@
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()
}
}
}
}