import SwiftUI struct YouTubeImportView: View { let folderNumber: Int let onComplete: () -> Void @EnvironmentObject private var app: AppModel @Environment(\.dismiss) private var dismiss @State private var urlText = "" @State private var video: YouTubeVideoInfo? @State private var splitByChapters = false @State private var isInspecting = false @State private var isImporting = false @State private var imported = false @State private var errorMessage: String? private var chapters: [YouTubeChapter] { video?.usableChapters ?? [] } var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 4) { Text("YouTube-Audio importieren").font(.title2.bold()) Text("Ziel: Ordner \(String(format: "%02d", folderNumber))") .foregroundStyle(.secondary) } HStack { TextField("https://www.youtube.com/watch?v=…", text: $urlText) .textFieldStyle(.roundedBorder) .onSubmit { inspectVideo() } .onChange(of: urlText) { _ in video = nil; imported = false; errorMessage = nil } Button("Video prüfen", systemImage: "magnifyingglass") { inspectVideo() } .disabled(isInspecting || isImporting || urlText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) } if isInspecting { ProgressView("YouTube-Video wird analysiert…") } else if let video { VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 3) { Text(video.title).font(.headline).lineLimit(2) Text([video.artist.isEmpty ? nil : video.artist, durationText(video.duration)].compactMap { $0 }.joined(separator: " · ")) .font(.caption).foregroundStyle(.secondary) } Picker("Importart", selection: $splitByChapters) { Text("Gesamtes Video als eine MP3").tag(false) Text("Einzeldatei je Kapitel (\(chapters.count))").tag(true).disabled(chapters.isEmpty) } if chapters.isEmpty { Text("Keine Kapitel gefunden. Das Video kann als eine Datei importiert werden.") .font(.caption).foregroundStyle(.secondary) } else { VStack(alignment: .leading, spacing: 4) { Text("Kapitelvorschau").font(.subheadline.weight(.semibold)) ForEach(Array(chapters.indices.prefix(6)), id: \.self) { index in let chapter = chapters[index] HStack(spacing: 8) { Text(String(format: "%02d", index + 1)).foregroundStyle(.secondary).monospacedDigit() Text(chapter.title?.isEmpty == false ? chapter.title! : "Kapitel \(index + 1)").lineLimit(1) Spacer() Text(durationText((chapter.end ?? video.duration).map { $0 - chapter.start })) .foregroundStyle(.secondary).monospacedDigit() }.font(.caption) } if chapters.count > 6 { Text("und \(chapters.count - 6) weitere Kapitel…").font(.caption).foregroundStyle(.secondary) } } .padding(12) .background(.quaternary.opacity(0.35), in: RoundedRectangle(cornerRadius: 8)) } } } if isImporting { ProgressView("Audio wird heruntergeladen, konvertiert und importiert…") } else if let errorMessage { Label(errorMessage, systemImage: "exclamationmark.triangle.fill") .font(.callout).foregroundStyle(.red).fixedSize(horizontal: false, vertical: true) } else if imported { Label("Audio wurde in Ordner \(String(format: "%02d", folderNumber)) importiert.", systemImage: "checkmark.circle.fill") .foregroundStyle(.green) } Spacer(minLength: 0) HStack { Text("yt-dlp lädt die Audiospur; FFmpeg erstellt MP3-Dateien mit Titel- und Interpret-Tags.") .font(.caption).foregroundStyle(.secondary).frame(maxWidth: .infinity, alignment: .leading) Button("Schließen") { dismiss() }.keyboardShortcut(.cancelAction) Button("Importieren", systemImage: "arrow.down.to.line") { importVideo() } .buttonStyle(.borderedProminent) .disabled(video == nil || isInspecting || isImporting || (splitByChapters && chapters.isEmpty)) } } .padding(22) .frame(width: 640, height: 540) } private func inspectVideo() { let value = urlText.trimmingCharacters(in: .whitespacesAndNewlines) guard !value.isEmpty else { return } isInspecting = true; errorMessage = nil; imported = false Task { do { let result = try await Task.detached(priority: .userInitiated) { try YouTubeImportService.inspect(value) }.value video = result splitByChapters = false isInspecting = false } catch { errorMessage = error.localizedDescription isInspecting = false } } } private func importVideo() { guard let video else { return } isImporting = true; errorMessage = nil; imported = false Task { do { try await app.library.importYouTube(video, splitByChapters: splitByChapters, to: folderNumber) isImporting = false; imported = true app.status = "YouTube-Audio in Ordner \(String(format: "%02d", folderNumber)) importiert" onComplete() } catch { isImporting = false; errorMessage = error.localizedDescription } } } private func durationText(_ seconds: Double?) -> String { guard let seconds, seconds.isFinite, seconds >= 0 else { return "" } let total = Int(seconds.rounded()) return total >= 3600 ? String(format: "%d:%02d:%02d", total / 3600, (total / 60) % 60, total % 60) : String(format: "%d:%02d", total / 60, total % 60) } }