You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

194 lines
7.1 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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

Powered by TurnKey Linux.