From 52a2c9bf83999d0582645632cdaa11c6ee4a1ec2 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Fri, 5 Jun 2026 08:08:38 +0200 Subject: [PATCH] Add project save and load controls --- Sources/GrowthLapse/ContentView.swift | 28 ++++++++++++++++++ Sources/GrowthLapse/VideoProcessor.swift | 36 ++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/Sources/GrowthLapse/ContentView.swift b/Sources/GrowthLapse/ContentView.swift index 3293d75..9aea0ee 100644 --- a/Sources/GrowthLapse/ContentView.swift +++ b/Sources/GrowthLapse/ContentView.swift @@ -39,6 +39,11 @@ struct ContentView: View { ) { selectOutputFile() } + + Button("Projekt laden") { + selectProjectFile() + } + .help("Lädt eine gespeicherte .growthlapse-project.json, inklusive Segmentfenstern und Variantenwahl.") } .disabled(processor.state.isRunning) @@ -365,6 +370,12 @@ struct ContentView: View { .disabled(processor.state.isRunning || !project.clips.contains(where: \.needsRender)) .help("Rendert nur geänderte normalisierte Clips neu und baut danach das finale Video erneut.") + Button("Projekt speichern") { + processor.saveCurrentProject() + } + .disabled(processor.state.isRunning) + .help("Speichert die aktuelle Nachbearbeitung in die .growthlapse-project.json neben der Output-Datei. Segmentänderungen werden zusätzlich automatisch gespeichert.") + Button("Ergebnis bestätigen") { processor.confirmProjectAndDeleteCache() } @@ -619,6 +630,23 @@ struct ContentView: View { } } + private func selectProjectFile() { + let panel = NSOpenPanel() + panel.title = "GrowthLapse-Projekt laden" + panel.canChooseFiles = true + panel.canChooseDirectories = false + panel.allowsMultipleSelection = false + panel.allowedContentTypes = [.json] + panel.directoryURL = settings.outputFile?.deletingLastPathComponent() + if panel.runModal() == .OK, + let url = panel.url, + let project = processor.loadProject(from: url) { + settings.outputFile = project.outputFileURL + settings.inputFolder = project.clips.first?.sourceURL.deletingLastPathComponent() + selectedClipID = project.clips.first?.id + } + } + private func selectExecutable(title: String, onSelect: (URL) -> Void) { let panel = NSOpenPanel() panel.title = title diff --git a/Sources/GrowthLapse/VideoProcessor.swift b/Sources/GrowthLapse/VideoProcessor.swift index c20364c..efb8911 100644 --- a/Sources/GrowthLapse/VideoProcessor.swift +++ b/Sources/GrowthLapse/VideoProcessor.swift @@ -137,6 +137,42 @@ final class VideoProcessor: ObservableObject { saveProject(reordered) } + func saveCurrentProject() { + guard let project else { + appendLog("Kein Projekt zum Speichern geladen.") + return + } + saveProject(project) + appendLog("Projekt gespeichert: \(GrowthLapseProject.url(for: project.outputFileURL).path)") + } + + func loadProject(from url: URL) -> GrowthLapseProject? { + do { + let data = try Data(contentsOf: url) + var loadedProject = try JSONDecoder().decode(GrowthLapseProject.self, from: data) + loadedProject.clips = loadedProject.clips.map { clip in + var updated = clip + if !FileManager.default.fileExists(atPath: updated.normalizedClipPath) { + updated.needsRender = true + } + return updated + } + project = loadedProject + state = .finished(loadedProject.outputFileURL) + progress = 1 + progressText = "Projekt geladen" + appendLog("Projekt geladen: \(url.path)") + if loadedProject.clips.contains(where: \.needsRender) { + appendLog("Hinweis: Einige Cache-Clips fehlen und werden beim nächsten Render neu erzeugt.") + } + return loadedProject + } catch { + state = .failed("Projekt konnte nicht geladen werden: \(error.localizedDescription)") + appendLog("Fehler: Projekt konnte nicht geladen werden: \(error.localizedDescription)") + return nil + } + } + func confirmProjectAndDeleteCache() { guard let project else { return