Show editable segment window timeline

This commit is contained in:
Mikei386
2026-06-04 09:38:15 +02:00
parent 3c7eede011
commit 7cb4c5e86a
+77 -6
View File
@@ -368,18 +368,19 @@ struct ContentView: View {
} }
private func clipEditor(_ clip: GrowthLapseProjectClip) -> some View { private func clipEditor(_ clip: GrowthLapseProjectClip) -> some View {
let maxStart = max(0, clip.duration - clip.segmentLength) let liveClip = currentClip(id: clip.id) ?? clip
let maxStart = max(0, liveClip.duration - liveClip.segmentLength)
let startBinding = Binding<Double>( let startBinding = Binding<Double>(
get: { currentClip(id: clip.id)?.segmentStart ?? clip.segmentStart }, get: { currentClip(id: clip.id)?.segmentStart ?? clip.segmentStart },
set: { processor.updateClip(clip, segmentStart: $0) } set: { processor.updateClip(clip, segmentStart: $0) }
) )
return VStack(alignment: .leading, spacing: 8) { return VStack(alignment: .leading, spacing: 8) {
ClipPreviewView(clip: currentClip(id: clip.id) ?? clip) ClipPreviewView(clip: liveClip)
.frame(height: 180) .frame(height: 180)
.clipShape(RoundedRectangle(cornerRadius: 6)) .clipShape(RoundedRectangle(cornerRadius: 6))
Text(clip.displayName) Text(liveClip.displayName)
.font(.subheadline.weight(.semibold)) .font(.subheadline.weight(.semibold))
.lineLimit(2) .lineLimit(2)
@@ -391,7 +392,16 @@ struct ContentView: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
Slider(value: startBinding, in: 0...maxStart, step: 0.1) SegmentTimelineView(clip: liveClip)
.frame(height: 42)
.help("Blau ist das verwendete Segmentfenster. Der markierte rechte Bereich kann nicht als Startpunkt gewählt werden, weil das Segment sonst über das Videoende hinauslaufen würde.")
if maxStart > 0 {
Slider(value: startBinding, in: 0...maxStart, step: 0.1)
} else {
Slider(value: .constant(0), in: 0...1)
.disabled(true)
}
HStack { HStack {
Button("-1 s") { Button("-1 s") {
@@ -401,7 +411,7 @@ struct ContentView: View {
processor.updateClip(clip, segmentStart: startBinding.wrappedValue - 0.5) processor.updateClip(clip, segmentStart: startBinding.wrappedValue - 0.5)
} }
Button("Zur Mitte") { Button("Zur Mitte") {
processor.updateClip(clip, segmentStart: max(0, (clip.duration - clip.segmentLength) / 2 + settings.startOffset)) processor.updateClip(clip, segmentStart: max(0, (liveClip.duration - liveClip.segmentLength) / 2 + settings.startOffset))
} }
Button("+0,5 s") { Button("+0,5 s") {
processor.updateClip(clip, segmentStart: startBinding.wrappedValue + 0.5) processor.updateClip(clip, segmentStart: startBinding.wrappedValue + 0.5)
@@ -410,7 +420,7 @@ struct ContentView: View {
processor.updateClip(clip, segmentStart: startBinding.wrappedValue + 1) processor.updateClip(clip, segmentStart: startBinding.wrappedValue + 1)
} }
} }
.disabled(processor.state.isRunning) .disabled(processor.state.isRunning || maxStart <= 0)
} }
.frame(maxWidth: .infinity, alignment: .topLeading) .frame(maxWidth: .infinity, alignment: .topLeading)
} }
@@ -554,3 +564,64 @@ private struct ClipPreviewView: View {
player.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero) player.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero)
} }
} }
private struct SegmentTimelineView: View {
let clip: GrowthLapseProjectClip
var body: some View {
VStack(alignment: .leading, spacing: 5) {
GeometryReader { proxy in
let width = max(1, proxy.size.width)
let duration = max(0.001, clip.duration)
let maxStart = max(0, clip.duration - clip.segmentLength)
let startX = width * CGFloat(clamp(clip.segmentStart / duration))
let segmentWidth = max(3, width * CGFloat(clamp(clip.segmentLength / duration)))
let invalidStartX = width * CGFloat(clamp(maxStart / duration))
let invalidWidth = max(0, width - invalidStartX)
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 4)
.fill(Color(nsColor: .separatorColor).opacity(0.35))
Rectangle()
.fill(Color.orange.opacity(0.16))
.frame(width: invalidWidth)
.offset(x: invalidStartX)
RoundedRectangle(cornerRadius: 4)
.fill(Color.accentColor.opacity(0.75))
.frame(width: min(segmentWidth, width - startX))
.offset(x: startX)
Rectangle()
.fill(Color.accentColor)
.frame(width: 2)
.offset(x: startX)
}
}
.frame(height: 14)
HStack {
Text("0 s")
Spacer()
Text("Segment \(format(clip.segmentStart))-\(format(min(clip.duration, clip.segmentStart + clip.segmentLength))) s")
Spacer()
Text("\(format(clip.duration)) s")
}
.font(.caption2.monospacedDigit())
.foregroundStyle(.secondary)
}
}
private func clamp(_ value: Double) -> Double {
min(1, max(0, value))
}
private func format(_ value: Double) -> String {
let formatter = NumberFormatter()
formatter.locale = .current
formatter.minimumFractionDigits = value.rounded() == value ? 0 : 1
formatter.maximumFractionDigits = 1
return formatter.string(from: NSNumber(value: value)) ?? "\(value)"
}
}