diff --git a/Sources/GrowthLapse/ContentView.swift b/Sources/GrowthLapse/ContentView.swift index 3885565..9502959 100644 --- a/Sources/GrowthLapse/ContentView.swift +++ b/Sources/GrowthLapse/ContentView.swift @@ -7,6 +7,7 @@ struct ContentView: View { @State private var settings = SettingsStore.load() @State private var selectedClipID: UUID? @State private var variantPickerClip: GrowthLapseProjectClip? + @State private var reviewPanelHeight: CGFloat = 340 var body: some View { HSplitView { @@ -231,12 +232,14 @@ struct ContentView: View { .progressViewStyle(.linear) if let project = processor.project { - VSplitView { + ResizableVerticalStack( + topHeight: $reviewPanelHeight, + minTopHeight: 260, + minBottomHeight: 160 + ) { projectReviewPanel(project) - .frame(minHeight: 260) - + } bottom: { logView - .frame(minHeight: 160) } } else { @@ -635,6 +638,92 @@ private struct ClipPreviewView: View { } } +private struct ResizableVerticalStack: View { + @Binding var topHeight: CGFloat + let minTopHeight: CGFloat + let minBottomHeight: CGFloat + @ViewBuilder let top: () -> Top + @ViewBuilder let bottom: () -> Bottom + + @State private var dragStartHeight: CGFloat? + + var body: some View { + GeometryReader { proxy in + let dividerHeight: CGFloat = 14 + let availableHeight = max(0, proxy.size.height - dividerHeight) + let maxTopHeight = max(minTopHeight, availableHeight - minBottomHeight) + let clampedTopHeight = min(max(topHeight, minTopHeight), maxTopHeight) + + VStack(spacing: 0) { + top() + .frame(height: clampedTopHeight) + .clipped() + + SplitHandle() + .frame(height: dividerHeight) + .contentShape(Rectangle()) + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { value in + if dragStartHeight == nil { + dragStartHeight = clampedTopHeight + } + let proposed = (dragStartHeight ?? clampedTopHeight) + value.translation.height + topHeight = min(max(proposed, minTopHeight), maxTopHeight) + } + .onEnded { _ in + dragStartHeight = nil + } + ) + .help("Ziehen, um Clip-Nachbearbeitung und Log größer oder kleiner zu machen.") + + bottom() + .frame(height: max(minBottomHeight, availableHeight - clampedTopHeight)) + .clipped() + } + .onAppear { + topHeight = clampedTopHeight + } + .onChange(of: proxy.size.height) { _ in + topHeight = clampedTopHeight + } + } + } +} + +private struct SplitHandle: View { + @State private var isHovering = false + + var body: some View { + ZStack { + Rectangle() + .fill(Color(nsColor: .windowBackgroundColor)) + + RoundedRectangle(cornerRadius: 3) + .fill(isHovering ? Color.accentColor.opacity(0.75) : Color(nsColor: .separatorColor)) + .frame(width: 84, height: 5) + } + .overlay(alignment: .top) { + Rectangle() + .fill(Color(nsColor: .separatorColor).opacity(0.45)) + .frame(height: 1) + } + .overlay(alignment: .bottom) { + Rectangle() + .fill(Color(nsColor: .separatorColor).opacity(0.45)) + .frame(height: 1) + } + .onHover { hovering in + isHovering = hovering + if hovering { + NSCursor.resizeUpDown.push() + } else { + NSCursor.pop() + } + } + } +} + private struct SegmentTimelineView: View { let clip: GrowthLapseProjectClip diff --git a/Sources/GrowthLapse/GrowthLapseApp.swift b/Sources/GrowthLapse/GrowthLapseApp.swift index 1025478..b586523 100644 --- a/Sources/GrowthLapse/GrowthLapseApp.swift +++ b/Sources/GrowthLapse/GrowthLapseApp.swift @@ -1,12 +1,34 @@ +import AppKit import SwiftUI @main struct GrowthLapseApp: App { + @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate + var body: some Scene { WindowGroup { ContentView() .frame(minWidth: 980, minHeight: 680) } .windowStyle(.titleBar) + .commands { + CommandGroup(replacing: .appTermination) { + Button("GrowthLapse beenden") { + NSApp.terminate(nil) + } + .keyboardShortcut("q", modifiers: .command) + } + } + } +} + +final class AppDelegate: NSObject, NSApplicationDelegate { + func applicationDidFinishLaunching(_ notification: Notification) { + NSApp.setActivationPolicy(.regular) + NSApp.activate(ignoringOtherApps: true) + } + + func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + true } }