You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

92 lines
3.3 KiB

import AVKit
import DropTwoCore
import SwiftUI
/// Schritt 5: 9:16-Master mit eingeblendetem 4:5-Rahmen zur Framing-Kontrolle.
/// Scrubbar über die nativen AVKit-Kontrollen von `VideoPlayer`. 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 {
VideoPlayer(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."
}
}
}

Powered by TurnKey Linux.