import SwiftUI struct ContentView: View { @EnvironmentObject private var settings: AppSettings @StateObject private var coordinator = ConversionCoordinator() @State private var showSettings = false @State private var isDropTargeted = false var body: some View { VStack(spacing: 0) { if coordinator.items.isEmpty { emptyState } else { FileListView() } Divider() statusBar } .frame(minWidth: 680, minHeight: 420) .navigationTitle("drop.two") .toolbar { toolbarContent } .overlay { if isDropTargeted { RoundedRectangle(cornerRadius: 8) .strokeBorder(Color.gold, lineWidth: 2) .padding(4) .allowsHitTesting(false) } } .onDrop(of: [.fileURL], isTargeted: $isDropTargeted) { providers in FileImport.loadURLs(from: providers) { coordinator.addFiles($0) } return true } .environmentObject(coordinator) #if DEBUG // Nur zum Ausprobieren: DROPTWO_DEBUG_FILES="/a.mp4:/b.mov" lädt // Dateien beim Start, ohne Dialog. .onAppear { if let paths = ProcessInfo.processInfo.environment["DROPTWO_DEBUG_FILES"] { coordinator.addFiles(paths.split(separator: ":").map { URL(fileURLWithPath: String($0)) }) } } #endif .sheet(isPresented: $showSettings) { SettingsView() } .alert( "Zieldatei existiert bereits", isPresented: Binding( get: { !coordinator.pendingConflicts.isEmpty }, set: { isPresented in if !isPresented { coordinator.cancelConflictResolution() } } ) ) { Button("Überspringen") { coordinator.resolveConflicts(overwrite: false) } Button("Ersetzen", role: .destructive) { coordinator.resolveConflicts(overwrite: true) } Button("Abbrechen", role: .cancel) { coordinator.cancelConflictResolution() } } message: { Text(conflictMessage) } } // MARK: - Toolbar @ToolbarContentBuilder private var toolbarContent: some ToolbarContent { ToolbarItemGroup(placement: .primaryAction) { Button(action: addFiles) { Label("Dateien hinzufügen", systemImage: "plus") } .help("Videos hinzufügen (⌘O)") .keyboardShortcut("o") Button(action: chooseTargetFolder) { Label { Text(settings.targetFolder?.lastPathComponent ?? String(localized: "Zielordner wählen …")) } icon: { Image(systemName: "folder") } .labelStyle(.titleAndIcon) } .help(settings.targetFolder.map { "Zielordner: \($0.path)" } ?? String(localized: "Hier entsteht pro Projekt ein Unterordner mit beiden Fassungen.")) Button("Start", action: start) .buttonStyle(ProminentButtonStyle()) .keyboardShortcut(.defaultAction) .disabled(coordinator.items.isEmpty || coordinator.isRunning) } ToolbarItem(placement: .automatic) { Button { showSettings = true } label: { Label("Einstellungen", systemImage: "gearshape") } .help("Einstellungen") } } // MARK: - Leerer Zustand /// Kein Symbolbild, keine Box: nur ein Satz. Das ganze Fenster nimmt Dateien an. private var emptyState: some View { VStack(spacing: 10) { Text("9:16-Videos hierher ziehen.") .font(.system(size: 22, weight: .medium)) HStack(spacing: 4) { Text("Oder") Button("Dateien wählen …", action: addFiles) .buttonStyle(.link) .tint(.gold) Text("· Daraus wird je eine 4:5-Fassung im Projektordner.") } .font(.system(size: 13)) .foregroundStyle(.secondary) } .frame(maxWidth: .infinity, maxHeight: .infinity) } // MARK: - Statusleiste private var statusBar: some View { HStack(spacing: 6) { if coordinator.items.isEmpty { Text(settings.targetFolder.map { "Ziel: \($0.path)" } ?? String(localized: "Noch kein Zielordner gewählt.")) .foregroundStyle(settings.targetFolder == nil ? Color.attention : .secondary) } else { Text(summary) .foregroundStyle(.secondary) if settings.targetFolder == nil { Text("· Kein Zielordner – wird beim Start abgefragt.") .foregroundStyle(Color.attention) } let missing = coordinator.itemsMissingFormat if missing.count > 1 && !coordinator.isRunning { Text("·").foregroundStyle(.secondary) Button("Bei allen \(missing.count) ohne Format „4x5“ anhängen") { coordinator.setAppendFormat(true, for: missing.map(\.id)) } .buttonStyle(.link) .tint(.gold) } } Spacer() if !coordinator.items.isEmpty { Button("Liste leeren") { coordinator.removeAll() } .buttonStyle(.link) .foregroundStyle(.secondary) .disabled(coordinator.isRunning) } } .font(.system(size: 11)) .lineLimit(1) .padding(.horizontal, 16) .frame(height: 28) .background(.bar) } private var summary: String { let total = coordinator.items.count let done = coordinator.finishedCount let files = total == 1 ? String(localized: "1 Datei") : String(localized: "\(total) Dateien") if coordinator.isRunning { return String(localized: "\(files) · \(done) fertig · rendert …") } return done > 0 ? String(localized: "\(files) · \(done) fertig") : files } // MARK: - Aktionen private func addFiles() { coordinator.addFiles(FileImport.chooseVideos(startingAt: settings.sourceFolder)) } private func chooseTargetFolder() { if let url = FolderPicker.choose(startingAt: settings.targetFolder, prompt: String(localized: "Als Zielordner wählen")) { settings.setTargetFolder(url) } } private func start() { if settings.targetFolder == nil { chooseTargetFolder() } guard let target = settings.targetFolder else { return } coordinator.start(targetFolder: target) } private var conflictMessage: String { let names = coordinator.pendingConflicts.map(\.fileName).joined(separator: ", ") return "Für folgende Datei(en) existiert die Zieldatei schon: \(names). Ersetzen oder überspringen?" } }