Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
05aedadfc3
commit
8c40e638e5
@ -25,12 +25,18 @@ struct ImageEditorFlow: View {
|
||||
imageType.cropAspectRatio
|
||||
}
|
||||
|
||||
/// Only allow aspect ratio selection for logos
|
||||
private var allowAspectRatioSelection: Bool {
|
||||
imageType == .logo
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// Source picker is the base content of this sheet
|
||||
sourcePickerView
|
||||
.fullScreenCover(isPresented: $showingFullScreenPicker) {
|
||||
PhotoPickerFlow(
|
||||
aspectRatio: aspectRatio,
|
||||
allowAspectRatioSelection: allowAspectRatioSelection,
|
||||
onComplete: { imageData in
|
||||
showingFullScreenPicker = false
|
||||
if let imageData {
|
||||
@ -43,6 +49,7 @@ struct ImageEditorFlow: View {
|
||||
.fullScreenCover(isPresented: $showingFullScreenCamera) {
|
||||
CameraFlow(
|
||||
aspectRatio: aspectRatio,
|
||||
allowAspectRatioSelection: allowAspectRatioSelection,
|
||||
onComplete: { imageData in
|
||||
showingFullScreenCamera = false
|
||||
if let imageData {
|
||||
@ -121,6 +128,7 @@ struct ImageEditorFlow: View {
|
||||
|
||||
private struct PhotoPickerFlow: View {
|
||||
let aspectRatio: CropAspectRatio
|
||||
let allowAspectRatioSelection: Bool
|
||||
let onComplete: (Data?) -> Void
|
||||
|
||||
@State private var selectedPhotoItem: PhotosPickerItem?
|
||||
@ -163,6 +171,7 @@ private struct PhotoPickerFlow: View {
|
||||
PhotoCropperSheet(
|
||||
imageData: imageData,
|
||||
aspectRatio: aspectRatio,
|
||||
allowAspectRatioSelection: allowAspectRatioSelection,
|
||||
shouldDismissOnComplete: false
|
||||
) { croppedData in
|
||||
if let croppedData {
|
||||
@ -186,6 +195,7 @@ private struct PhotoPickerFlow: View {
|
||||
|
||||
private struct CameraFlow: View {
|
||||
let aspectRatio: CropAspectRatio
|
||||
let allowAspectRatioSelection: Bool
|
||||
let onComplete: (Data?) -> Void
|
||||
|
||||
@State private var capturedImageData: Data?
|
||||
@ -215,6 +225,7 @@ private struct CameraFlow: View {
|
||||
PhotoCropperSheet(
|
||||
imageData: imageData,
|
||||
aspectRatio: aspectRatio,
|
||||
allowAspectRatioSelection: allowAspectRatioSelection,
|
||||
shouldDismissOnComplete: false
|
||||
) { croppedData in
|
||||
if let croppedData {
|
||||
|
||||
@ -2,44 +2,94 @@ import SwiftUI
|
||||
import Bedrock
|
||||
|
||||
/// Aspect ratio options for the photo cropper
|
||||
enum CropAspectRatio {
|
||||
enum CropAspectRatio: Identifiable, CaseIterable, Equatable {
|
||||
case original // Use image's original aspect ratio
|
||||
case square // 1:1 for profile photos
|
||||
case threeToTwo // 3:2
|
||||
case fiveToThree // 5:3
|
||||
case fourToThree // 4:3
|
||||
case fiveToFour // 5:4
|
||||
case sevenToFive // 7:5
|
||||
case sixteenToNine // 16:9
|
||||
case banner // Wide ratio for cover/banner photos (roughly 2.3:1)
|
||||
case custom(CGFloat) // Custom width:height ratio
|
||||
|
||||
var ratio: CGFloat {
|
||||
var id: String { displayName }
|
||||
|
||||
static var allCases: [CropAspectRatio] {
|
||||
[.original, .square, .threeToTwo, .fiveToThree, .fourToThree, .fiveToFour, .sevenToFive, .sixteenToNine]
|
||||
}
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .original: return String.localized("Original")
|
||||
case .square: return String.localized("Square")
|
||||
case .threeToTwo: return "3:2"
|
||||
case .fiveToThree: return "5:3"
|
||||
case .fourToThree: return "4:3"
|
||||
case .fiveToFour: return "5:4"
|
||||
case .sevenToFive: return "7:5"
|
||||
case .sixteenToNine: return "16:9"
|
||||
case .banner: return String.localized("Banner")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the ratio (width / height). For .original, pass the image size.
|
||||
func ratio(for imageSize: CGSize? = nil) -> CGFloat {
|
||||
switch self {
|
||||
case .original:
|
||||
guard let size = imageSize, size.height > 0 else { return 1.0 }
|
||||
return size.width / size.height
|
||||
case .square:
|
||||
return 1.0
|
||||
case .threeToTwo:
|
||||
return 3.0 / 2.0
|
||||
case .fiveToThree:
|
||||
return 5.0 / 3.0
|
||||
case .fourToThree:
|
||||
return 4.0 / 3.0
|
||||
case .fiveToFour:
|
||||
return 5.0 / 4.0
|
||||
case .sevenToFive:
|
||||
return 7.0 / 5.0
|
||||
case .sixteenToNine:
|
||||
return 16.0 / 9.0
|
||||
case .banner:
|
||||
return 2.3 // Width is 2.3x height
|
||||
case .custom(let ratio):
|
||||
return ratio
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple ratio for backwards compatibility (doesn't handle .original properly)
|
||||
var ratio: CGFloat {
|
||||
ratio(for: nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// A sheet that allows the user to crop an image.
|
||||
/// Supports pinch-to-zoom and drag gestures for positioning.
|
||||
/// Use `aspectRatio` to specify square, banner, or custom crop shapes.
|
||||
/// Set `allowAspectRatioSelection` to true to show aspect ratio picker (for logos).
|
||||
struct PhotoCropperSheet: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
let imageData: Data
|
||||
let aspectRatio: CropAspectRatio
|
||||
let initialAspectRatio: CropAspectRatio
|
||||
let allowAspectRatioSelection: Bool
|
||||
let onComplete: (Data?) -> Void // nil = cancelled, Data = saved
|
||||
let shouldDismissOnComplete: Bool
|
||||
|
||||
init(
|
||||
imageData: Data,
|
||||
aspectRatio: CropAspectRatio = .square,
|
||||
allowAspectRatioSelection: Bool = false,
|
||||
shouldDismissOnComplete: Bool = true,
|
||||
onComplete: @escaping (Data?) -> Void
|
||||
) {
|
||||
self.imageData = imageData
|
||||
self.aspectRatio = aspectRatio
|
||||
self.initialAspectRatio = aspectRatio
|
||||
self.allowAspectRatioSelection = allowAspectRatioSelection
|
||||
self.shouldDismissOnComplete = shouldDismissOnComplete
|
||||
self.onComplete = onComplete
|
||||
self._selectedAspectRatio = State(initialValue: aspectRatio)
|
||||
}
|
||||
|
||||
@State private var scale: CGFloat = 1.0
|
||||
@ -50,25 +100,47 @@ struct PhotoCropperSheet: View {
|
||||
@State private var lastRotation: Angle = .zero
|
||||
@State private var fixedRotation: Angle = .zero // For 90° toolbar rotations
|
||||
@State private var containerSize: CGSize = .zero
|
||||
@State private var selectedAspectRatio: CropAspectRatio
|
||||
@State private var showingAspectRatioPicker = false
|
||||
|
||||
// Horizontal padding for the crop area
|
||||
private let horizontalPadding: CGFloat = 20
|
||||
|
||||
/// Current effective aspect ratio (accounting for image size for .original)
|
||||
private var effectiveRatio: CGFloat {
|
||||
selectedAspectRatio.ratio(for: uiImage?.size)
|
||||
}
|
||||
|
||||
private var cropWidth: CGFloat {
|
||||
// Use almost full width for better cropping experience
|
||||
max(100, containerSize.width - (horizontalPadding * 2))
|
||||
}
|
||||
|
||||
private var cropHeight: CGFloat {
|
||||
cropWidth / aspectRatio.ratio
|
||||
// Calculate height based on aspect ratio
|
||||
let height = cropWidth / effectiveRatio
|
||||
// Ensure it fits within the container
|
||||
if height > containerSize.height - 100 {
|
||||
return containerSize.height - 100
|
||||
}
|
||||
return height
|
||||
}
|
||||
|
||||
private var adjustedCropWidth: CGFloat {
|
||||
// If height was constrained, recalculate width
|
||||
let height = cropWidth / effectiveRatio
|
||||
if height > containerSize.height - 100 {
|
||||
return cropHeight * effectiveRatio
|
||||
}
|
||||
return cropWidth
|
||||
}
|
||||
|
||||
private var cropSize: CGSize {
|
||||
// Guard against zero/invalid sizes
|
||||
guard containerSize.width > 0, containerSize.height > 0 else {
|
||||
return CGSize(width: 280, height: 280 / aspectRatio.ratio)
|
||||
return CGSize(width: 280, height: 280 / effectiveRatio)
|
||||
}
|
||||
return CGSize(width: cropWidth, height: cropHeight)
|
||||
return CGSize(width: adjustedCropWidth, height: cropHeight)
|
||||
}
|
||||
|
||||
private var uiImage: UIImage? {
|
||||
@ -140,6 +212,16 @@ struct PhotoCropperSheet: View {
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
|
||||
// Aspect ratio picker (only for logo workflow)
|
||||
if allowAspectRatioSelection {
|
||||
Button {
|
||||
showingAspectRatioPicker = true
|
||||
} label: {
|
||||
Image(systemName: "aspectratio")
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
|
||||
// Rotate 90° right
|
||||
Button {
|
||||
rotate90Right()
|
||||
@ -159,6 +241,20 @@ struct PhotoCropperSheet: View {
|
||||
}
|
||||
}
|
||||
.preferredColorScheme(.dark)
|
||||
.confirmationDialog(
|
||||
String.localized("Aspect Ratio"),
|
||||
isPresented: $showingAspectRatioPicker,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
ForEach(CropAspectRatio.allCases) { ratio in
|
||||
Button(ratio.displayName) {
|
||||
withAnimation(.spring(duration: Design.Animation.springDuration)) {
|
||||
selectedAspectRatio = ratio
|
||||
}
|
||||
}
|
||||
}
|
||||
Button(String.localized("Cancel"), role: .cancel) { }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Gestures
|
||||
@ -335,13 +431,17 @@ struct PhotoCropperSheet: View {
|
||||
|
||||
// Resize to a consistent output size based on aspect ratio
|
||||
let outputSize: CGSize
|
||||
switch aspectRatio {
|
||||
let baseWidth: CGFloat = 1024
|
||||
let currentRatio = effectiveRatio
|
||||
|
||||
switch selectedAspectRatio {
|
||||
case .square:
|
||||
outputSize = CGSize(width: 512, height: 512)
|
||||
case .banner:
|
||||
outputSize = CGSize(width: 1024, height: 445) // ~2.3:1 ratio
|
||||
case .custom(let ratio):
|
||||
outputSize = CGSize(width: 1024, height: 1024 / ratio)
|
||||
outputSize = CGSize(width: baseWidth, height: baseWidth / 2.3)
|
||||
default:
|
||||
// For all other ratios, use base width and calculate height
|
||||
outputSize = CGSize(width: baseWidth, height: baseWidth / currentRatio)
|
||||
}
|
||||
|
||||
let format = UIGraphicsImageRendererFormat()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user