import AVKit import DropTwoCore import SwiftUI /// Schritt 5: 9:16-Master mit eingeblendetem 4:5-Rahmen zur Framing-Kontrolle. /// Scrubbar über die nativen Kontrollen von `PlayerView` (AppKits /// `AVPlayerView`, nicht AVKits SwiftUI-`VideoPlayer` — siehe dort für den /// Grund). Der Rahmen nutzt exakt dieselbe Geometrie wie /// `CropEngine.targetSize` — was hier zu sehen ist, ist tatsächlich das, was /// beim Crop rauskommt. Rein zur Kontrolle: der Offset ist hier noch nicht /// verstellbar (README, "Danach"-Liste). struct ReviewSheet: View { let sourceURL: URL @Environment(\.dismiss) private var dismiss @State private var player: AVPlayer? @State private var videoSize: CGSize? @State private var loadError: String? var body: some View { VStack(spacing: 12) { HStack { Text(sourceURL.lastPathComponent) .font(.headline) .lineLimit(1) .truncationMode(.middle) Spacer() Button("Schließen") { dismiss() } } content } .padding(16) .frame(minWidth: 420, minHeight: 520) .task { await load() } } @ViewBuilder private var content: some View { if let loadError { VStack(spacing: 8) { Image(systemName: "exclamationmark.triangle") .font(.title) Text(loadError) } .foregroundStyle(.secondary) .frame(maxWidth: .infinity, maxHeight: .infinity) } else if let player, let videoSize, videoSize.height > 0 { GeometryReader { proxy in ZStack { PlayerView(player: player) cropOverlay(sourceSize: videoSize, viewSize: proxy.size) } } .aspectRatio(videoSize.width / videoSize.height, contentMode: .fit) } else { ProgressView("Lädt Vorschau …") .frame(maxWidth: .infinity, maxHeight: .infinity) } } /// Zwei abgedunkelte Balken oben/unten markieren, was beim Crop wegfällt; /// der helle Rahmen zeigt die bleibende 4:5-Fläche — volle Breite, /// vertikal zentriert, exakt wie `CropEngine` es tatsächlich schneidet. @ViewBuilder private func cropOverlay(sourceSize: CGSize, viewSize: CGSize) -> some View { let cropSize = CropEngine.targetSize(forSourceSize: sourceSize) let marginFraction = max(0, (1 - cropSize.height / sourceSize.height) / 2) let marginHeight = viewSize.height * marginFraction VStack(spacing: 0) { Rectangle() .fill(Color.black.opacity(0.55)) .frame(height: marginHeight) Rectangle() .strokeBorder(Color(red: 0.784, green: 0.663, blue: 0.416), lineWidth: 2) .frame(height: max(viewSize.height - marginHeight * 2, 0)) Rectangle() .fill(Color.black.opacity(0.55)) .frame(height: marginHeight) } .allowsHitTesting(false) } private func load() async { do { let info = try await VideoInspector.inspect(sourceURL) videoSize = info.pixelSize player = AVPlayer(url: sourceURL) } catch { loadError = "Konnte Vorschau nicht laden." } } }