|
|
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?"
|
|
|
}
|
|
|
}
|