diff --git a/Sources/GrowthLapse/ContentView.swift b/Sources/GrowthLapse/ContentView.swift index 6434de3..0ba5268 100644 --- a/Sources/GrowthLapse/ContentView.swift +++ b/Sources/GrowthLapse/ContentView.swift @@ -106,7 +106,7 @@ struct ContentView: View { Text(sortMode.rawValue).tag(sortMode) } } - .help("Bestimmt die chronologische Reihenfolge der Quellvideos: nach Dateiname oder Änderungsdatum.") + .help("Bestimmt die chronologische Reihenfolge der Quellvideos. Wenn Dateinamen wie '118 Wochen alt' enthalten sind, nutze 'Alter in Wochen aufsteigend'.") Picker("Mehrfach-Versionen", selection: $settings.duplicateClipSelection) { ForEach(DuplicateClipSelection.allCases) { selection in diff --git a/Sources/GrowthLapse/RenderSettings.swift b/Sources/GrowthLapse/RenderSettings.swift index a0ba7a2..fca5240 100644 --- a/Sources/GrowthLapse/RenderSettings.swift +++ b/Sources/GrowthLapse/RenderSettings.swift @@ -10,6 +10,7 @@ enum OutputFormat: String, CaseIterable, Identifiable, Codable { enum SortMode: String, CaseIterable, Identifiable, Codable { case filenameAscending = "Dateiname aufsteigend" + case ageWeeksAscending = "Alter in Wochen aufsteigend" case modificationDateAscending = "Änderungsdatum aufsteigend" var id: String { rawValue } diff --git a/Sources/GrowthLapse/VideoProcessor.swift b/Sources/GrowthLapse/VideoProcessor.swift index e497278..6073344 100644 --- a/Sources/GrowthLapse/VideoProcessor.swift +++ b/Sources/GrowthLapse/VideoProcessor.swift @@ -113,7 +113,7 @@ final class VideoProcessor: ObservableObject { settings: effectiveSettings, outputFile: outputFile, targetDimensions: targetDimensions, - inputClipCount: videos.count + selectedVideos: videos ) appendLog("Render-Protokoll: \(reportURL.path)") @@ -204,6 +204,15 @@ final class VideoProcessor: ObservableObject { switch sortMode { case .filenameAscending: urls.sort { $0.lastPathComponent.localizedStandardCompare($1.lastPathComponent) == .orderedAscending } + case .ageWeeksAscending: + urls.sort { left, right in + let leftAge = ageInWeeks(from: left) + let rightAge = ageInWeeks(from: right) + if leftAge != rightAge { + return (leftAge ?? Int.max) < (rightAge ?? Int.max) + } + return left.lastPathComponent.localizedStandardCompare(right.lastPathComponent) == .orderedAscending + } case .modificationDateAscending: urls.sort { let left = (try? $0.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? .distantPast @@ -264,6 +273,18 @@ final class VideoProcessor: ObservableObject { return selected } + private func ageInWeeks(from url: URL) -> Int? { + let name = url.deletingPathExtension().lastPathComponent + let pattern = #"(\d{1,4})\s*Wochen\s+alt"# + guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]), + let match = regex.firstMatch(in: name, range: NSRange(name.startIndex..., in: name)), + match.numberOfRanges == 2, + let range = Range(match.range(at: 1), in: name) else { + return nil + } + return Int(name[range]) + } + private func chooseDuplicateVariant(from variants: [VideoFile], selection: DuplicateClipSelection) -> VideoFile { let sortedVariants = variants.sorted { left, right in let leftVersion = duplicateVersionNumber(for: left.url) ?? Int.max @@ -949,21 +970,36 @@ final class VideoProcessor: ObservableObject { settings: RenderSettings, outputFile: URL, targetDimensions: VideoDimensions, - inputClipCount: Int + selectedVideos: [VideoFile] ) -> URL { let reportURL = outputFile .deletingPathExtension() .appendingPathExtension("growthlapse-report.txt") + let selectedClipLines = selectedVideos.enumerated().map { index, video in + let age = ageInWeeks(from: video.url).map { ", \($0) Wochen" } ?? "" + return String( + format: "%03d. %@ - %@, %dx%d%@", + index + 1, + video.displayName, + formatSeconds(video.duration), + video.width, + video.height, + age + ) + } let summary = [ "GrowthLapse Render-Protokoll", "Datum: \(Date())", + "Input-Ordner: \(settings.inputFolder?.path ?? "-")", "Output: \(outputFile.path)", - "Input-Clips: \(inputClipCount)", + "Input-Clips: \(selectedVideos.count)", "Zielauflösung: \(targetDimensions.width)x\(targetDimensions.height)", "Format: \(settings.outputFormat.rawValue)", "Encoder: \(settings.videoEncoder.rawValue)", "FPS: \(settings.fps)", + "Sortierung: \(settings.sortMode.rawValue)", + "Mehrfach-Versionen: \(settings.duplicateClipSelection.rawValue)", "Segmentlänge: \(settings.segmentLength)s", "Ziellänge pro Clip: \(settings.targetClipLength)s", "Übergangslänge: \(settings.transitionLength)s", @@ -974,6 +1010,9 @@ final class VideoProcessor: ObservableObject { "Stabilisierung Hintergrund-Anker: \(settings.stabilizationAnchorEnabled ? "an, X \(Int(settings.stabilizationAnchorX * 100))%, Y \(Int(settings.stabilizationAnchorY * 100))%, Bereich \(Int(settings.stabilizationAnchorSize * 100))%" : "aus")", "Face Size Normalization: \(settings.faceNormalizationEnabled ? "an, Ziel \(Int(settings.targetFaceHeightRatio * 100))%" : "aus")", "", + "Ausgewählte Clip-Reihenfolge:", + selectedClipLines.joined(separator: "\n"), + "", "Log:", logs.joined(separator: "\n") ].joined(separator: "\n")