712 lines
28 KiB
Swift
712 lines
28 KiB
Swift
//
|
|
// CustomCameraScreen.swift
|
|
// CameraTester
|
|
//
|
|
// Created by Matt Bruce on 1/2/26.
|
|
//
|
|
|
|
import AVFoundation
|
|
import SwiftUI
|
|
import Bedrock
|
|
import MijickCamera
|
|
|
|
// MARK: - Custom Camera Screen
|
|
|
|
struct CustomCameraScreen: MCameraScreen {
|
|
@ObservedObject var cameraManager: CameraManager
|
|
let namespace: Namespace.ID
|
|
let closeMCameraAction: () -> ()
|
|
|
|
/// Shared camera settings state - using Observable class prevents MCamera recreation
|
|
var cameraSettings: CameraSettingsState
|
|
|
|
/// Callback when photo is captured - bypasses MijickCamera's callback system
|
|
var onPhotoCaptured: ((UIImage) -> Void)?
|
|
|
|
// Convenience accessors for settings
|
|
private var photoQuality: PhotoQuality {
|
|
get { cameraSettings.photoQuality }
|
|
nonmutating set { cameraSettings.photoQuality = newValue }
|
|
}
|
|
private var isRingLightEnabled: Bool {
|
|
get { cameraSettings.isRingLightEnabled }
|
|
nonmutating set { cameraSettings.isRingLightEnabled = newValue }
|
|
}
|
|
private var ringLightColor: Color {
|
|
get { cameraSettings.ringLightColor }
|
|
nonmutating set { cameraSettings.ringLightColor = newValue }
|
|
}
|
|
private var ringLightSize: CGFloat {
|
|
get { cameraSettings.ringLightSize }
|
|
nonmutating set { cameraSettings.ringLightSize = newValue }
|
|
}
|
|
private var ringLightOpacity: Double {
|
|
get { cameraSettings.ringLightOpacity }
|
|
nonmutating set { cameraSettings.ringLightOpacity = newValue }
|
|
}
|
|
private var flashMode: CameraFlashMode {
|
|
get { cameraSettings.flashMode }
|
|
nonmutating set { cameraSettings.flashMode = newValue }
|
|
}
|
|
private var isFlashSyncedWithRingLight: Bool {
|
|
get { cameraSettings.isFlashSyncedWithRingLight }
|
|
nonmutating set { cameraSettings.isFlashSyncedWithRingLight = newValue }
|
|
}
|
|
|
|
// Center Stage state
|
|
@State private var isCenterStageEnabled: Bool = AVCaptureDevice.isCenterStageEnabled
|
|
|
|
// Controls panel expansion state
|
|
@State private var isControlsExpanded: Bool = false
|
|
|
|
// Ring light settings overlay state
|
|
@State private var showRingLightColorPicker: Bool = false
|
|
@State private var showRingLightSizeSlider: Bool = false
|
|
@State private var showRingLightOpacitySlider: Bool = false
|
|
|
|
// Screen flash state for front camera
|
|
@State private var isShowingScreenFlash: Bool = false
|
|
@State private var originalBrightness: CGFloat = UIScreen.main.brightness
|
|
|
|
// Pinch to zoom gesture state
|
|
@GestureState private var magnification: CGFloat = 1.0
|
|
@State private var lastMagnification: CGFloat = 1.0
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Camera preview with pinch gesture - Metal layer doesn't respect SwiftUI clipping
|
|
createCameraOutputView()
|
|
.ignoresSafeArea()
|
|
.gesture(
|
|
MagnificationGesture()
|
|
.updating($magnification) { currentState, gestureState, transaction in
|
|
gestureState = currentState
|
|
}
|
|
.onEnded { value in
|
|
let newZoom = lastMagnification * value
|
|
lastMagnification = newZoom
|
|
// Clamp to reasonable range
|
|
let clampedZoom = min(max(newZoom, 1.0), 5.0)
|
|
do {
|
|
try setZoomFactor(clampedZoom)
|
|
} catch {
|
|
print("Failed to set zoom factor: \(error)")
|
|
}
|
|
}
|
|
)
|
|
|
|
// Ring light overlay - covers corners and creates rounded inner edge
|
|
// When ring light is off, still show black corners to maintain rounded appearance
|
|
RingLightOverlay(
|
|
color: isRingLightEnabled ? ringLightColor : .black,
|
|
width: isRingLightEnabled ? ringLightSize : Design.CornerRadius.large,
|
|
opacity: isRingLightEnabled ? ringLightOpacity : 1.0,
|
|
cornerRadius: Design.CornerRadius.large
|
|
)
|
|
.allowsHitTesting(false) // Allow touches to pass through to camera view
|
|
|
|
// UI overlay - responsive to orientation
|
|
GeometryReader { geometry in
|
|
let isLandscape = geometry.size.width > geometry.size.height
|
|
|
|
if isLandscape {
|
|
// Landscape layout: full-width centered controls, capture button on left with zoom above
|
|
ZStack {
|
|
// Centered controls across entire screen
|
|
VStack(spacing: 0) {
|
|
// Top controls area - expandable panel (centered)
|
|
ExpandableControlsPanel(
|
|
isExpanded: $isControlsExpanded,
|
|
hasActiveSettings: hasActiveSettings,
|
|
activeSettingsIcons: activeSettingsIcons,
|
|
flashMode: flashMode,
|
|
flashIcon: flashIcon,
|
|
onFlashTap: toggleFlash,
|
|
isFlashSyncedWithRingLight: isFlashSyncedWithRingLight,
|
|
onFlashSyncTap: toggleFlashSync,
|
|
hdrMode: cameraSettings.hdrMode,
|
|
hdrIcon: hdrIcon,
|
|
onHDRTap: toggleHDR,
|
|
isGridVisible: isGridVisible,
|
|
gridIcon: gridIcon,
|
|
onGridTap: toggleGrid,
|
|
photoQuality: photoQuality,
|
|
onQualityTap: cycleQuality,
|
|
isCenterStageAvailable: isCenterStageAvailable,
|
|
isCenterStageEnabled: isCenterStageEnabled,
|
|
onCenterStageTap: toggleCenterStage,
|
|
isFrontCamera: cameraPosition == .front,
|
|
onFlipCameraTap: flipCamera,
|
|
isRingLightEnabled: isRingLightEnabled,
|
|
onRingLightTap: toggleRingLight,
|
|
ringLightColor: ringLightColor,
|
|
onRingLightColorTap: toggleRingLightColorPicker,
|
|
ringLightSize: ringLightSize,
|
|
onRingLightSizeTap: toggleRingLightSizeSlider,
|
|
ringLightOpacity: ringLightOpacity,
|
|
onRingLightOpacityTap: toggleRingLightOpacitySlider
|
|
)
|
|
.padding(.horizontal, Design.Spacing.large)
|
|
.padding(.top, Design.Spacing.medium)
|
|
|
|
Spacer()
|
|
}
|
|
|
|
// Left side overlay - Capture Button only
|
|
VStack {
|
|
Spacer()
|
|
CaptureButton(action: { performCapture() })
|
|
Spacer()
|
|
}
|
|
.padding(.leading, Design.Spacing.large)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
|
}
|
|
} else {
|
|
// Portrait layout: controls on top, capture button at bottom
|
|
VStack(spacing: 0) {
|
|
// Top controls area - expandable panel
|
|
ExpandableControlsPanel(
|
|
isExpanded: $isControlsExpanded,
|
|
hasActiveSettings: hasActiveSettings,
|
|
activeSettingsIcons: activeSettingsIcons,
|
|
flashMode: flashMode,
|
|
flashIcon: flashIcon,
|
|
onFlashTap: toggleFlash,
|
|
isFlashSyncedWithRingLight: isFlashSyncedWithRingLight,
|
|
onFlashSyncTap: toggleFlashSync,
|
|
hdrMode: cameraSettings.hdrMode,
|
|
hdrIcon: hdrIcon,
|
|
onHDRTap: toggleHDR,
|
|
isGridVisible: isGridVisible,
|
|
gridIcon: gridIcon,
|
|
onGridTap: toggleGrid,
|
|
photoQuality: photoQuality,
|
|
onQualityTap: cycleQuality,
|
|
isCenterStageAvailable: isCenterStageAvailable,
|
|
isCenterStageEnabled: isCenterStageEnabled,
|
|
onCenterStageTap: toggleCenterStage,
|
|
isFrontCamera: cameraPosition == .front,
|
|
onFlipCameraTap: flipCamera,
|
|
isRingLightEnabled: isRingLightEnabled,
|
|
onRingLightTap: toggleRingLight,
|
|
ringLightColor: ringLightColor,
|
|
onRingLightColorTap: toggleRingLightColorPicker,
|
|
ringLightSize: ringLightSize,
|
|
onRingLightSizeTap: toggleRingLightSizeSlider,
|
|
ringLightOpacity: ringLightOpacity,
|
|
onRingLightOpacityTap: toggleRingLightOpacitySlider
|
|
)
|
|
.padding(.horizontal, Design.Spacing.large)
|
|
.padding(.top, Design.Spacing.medium)
|
|
|
|
Spacer()
|
|
|
|
// Bottom controls
|
|
VStack(spacing: Design.Spacing.large) {
|
|
// Zoom indicator (shows "Center Stage" when active)
|
|
ZoomControlView(
|
|
zoomFactor: zoomFactor,
|
|
isCenterStageActive: isCenterStageEnabled
|
|
)
|
|
|
|
// Capture Button
|
|
CaptureButton(action: { performCapture() })
|
|
.padding(.bottom, Design.Spacing.large)
|
|
}
|
|
.padding(.horizontal, Design.Spacing.large)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ring light color picker overlay
|
|
if showRingLightColorPicker {
|
|
ColorPickerOverlay(
|
|
selectedColor: Binding(
|
|
get: { cameraSettings.ringLightColor },
|
|
set: { cameraSettings.ringLightColor = $0 }
|
|
),
|
|
isPresented: $showRingLightColorPicker
|
|
)
|
|
.transition(.opacity)
|
|
}
|
|
|
|
// Ring light size slider overlay
|
|
if showRingLightSizeSlider {
|
|
SizeSliderOverlay(
|
|
selectedSize: Binding(
|
|
get: { cameraSettings.ringLightSize },
|
|
set: { cameraSettings.ringLightSize = $0 }
|
|
),
|
|
isPresented: $showRingLightSizeSlider
|
|
)
|
|
.transition(.opacity)
|
|
}
|
|
|
|
// Ring light opacity slider overlay
|
|
if showRingLightOpacitySlider {
|
|
OpacitySliderOverlay(
|
|
selectedOpacity: Binding(
|
|
get: { cameraSettings.ringLightOpacity },
|
|
set: { cameraSettings.ringLightOpacity = $0 }
|
|
),
|
|
isPresented: $showRingLightOpacitySlider
|
|
)
|
|
.transition(.opacity)
|
|
}
|
|
|
|
// Screen flash overlay for front camera
|
|
if isShowingScreenFlash {
|
|
screenFlashColor
|
|
.ignoresSafeArea()
|
|
.transition(.opacity)
|
|
}
|
|
}
|
|
.animation(.easeInOut(duration: 0.05), value: isShowingScreenFlash)
|
|
.gesture(
|
|
// Only add tap gesture when there are overlays to dismiss
|
|
(isControlsExpanded || showRingLightColorPicker || showRingLightSizeSlider || showRingLightOpacitySlider) ?
|
|
TapGesture().onEnded {
|
|
// Collapse panel when tapping outside
|
|
if isControlsExpanded {
|
|
isControlsExpanded = false
|
|
}
|
|
// Hide overlays when tapping outside
|
|
if showRingLightColorPicker {
|
|
showRingLightColorPicker = false
|
|
}
|
|
if showRingLightSizeSlider {
|
|
showRingLightSizeSlider = false
|
|
}
|
|
if showRingLightOpacitySlider {
|
|
showRingLightOpacitySlider = false
|
|
}
|
|
} : nil
|
|
)
|
|
.onAppear {
|
|
// Set flash mode from saved settings
|
|
setFlashMode(cameraSettings.flashMode.toMijickFlashMode)
|
|
// Tell MijickCamera whether to disable iOS flash (only matters if sync is on)
|
|
updateFlashSyncState()
|
|
// Initialize zoom gesture state
|
|
lastMagnification = zoomFactor
|
|
}
|
|
.onChange(of: isFlashSyncedWithRingLight) { _, _ in
|
|
// Only update when sync setting changes, not on every color change
|
|
updateFlashSyncState()
|
|
}
|
|
.onChange(of: cameraManager.capturedMedia) { _, newMedia in
|
|
// Directly observe capture completion - bypasses MijickCamera's callback issues
|
|
if let media = newMedia, let image = media.getImage() {
|
|
print("CustomCameraScreen detected captured media!")
|
|
onPhotoCaptured?(image)
|
|
// Clear the captured media so next capture works
|
|
cameraManager.setCapturedMedia(nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Active Settings Detection
|
|
|
|
/// Returns true if any setting is in a non-default state
|
|
private var hasActiveSettings: Bool {
|
|
flashMode != .off || cameraSettings.hdrMode != .off || isGridVisible || isCenterStageEnabled || isRingLightEnabled
|
|
}
|
|
|
|
/// Returns icons for currently active settings (for collapsed pill display)
|
|
private var activeSettingsIcons: [String] {
|
|
var icons: [String] = []
|
|
if flashMode != .off {
|
|
icons.append(flashIcon)
|
|
}
|
|
if cameraSettings.hdrMode != .off {
|
|
icons.append(hdrIcon)
|
|
}
|
|
if isGridVisible {
|
|
icons.append(gridIcon)
|
|
}
|
|
if isRingLightEnabled {
|
|
icons.append("circle.fill")
|
|
}
|
|
if isCenterStageEnabled {
|
|
icons.append("person.crop.rectangle.fill")
|
|
}
|
|
return icons
|
|
}
|
|
|
|
// MARK: - Control Icons
|
|
private var flashIcon: String {
|
|
flashMode.icon
|
|
}
|
|
|
|
private var hdrIcon: String {
|
|
switch cameraSettings.hdrMode {
|
|
case .off: return "circle.lefthalf.filled"
|
|
case .auto: return "circle.lefthalf.filled"
|
|
case .on: return "circle.fill"
|
|
}
|
|
}
|
|
|
|
private var gridIcon: String {
|
|
isGridVisible ? "grid" : "grid"
|
|
}
|
|
|
|
private var isCenterStageAvailable: Bool {
|
|
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else {
|
|
return false
|
|
}
|
|
return device.activeFormat.isCenterStageSupported
|
|
}
|
|
|
|
// MARK: - Actions
|
|
private func toggleFlash() {
|
|
let nextMode: CameraFlashMode
|
|
switch flashMode {
|
|
case .off: nextMode = .auto
|
|
case .auto: nextMode = .on
|
|
case .on: nextMode = .off
|
|
}
|
|
flashMode = nextMode
|
|
// Update MijickCamera's flash mode so it knows to use iOS Retina Flash
|
|
setFlashMode(nextMode.toMijickFlashMode)
|
|
}
|
|
|
|
private func toggleFlashSync() {
|
|
isFlashSyncedWithRingLight.toggle()
|
|
}
|
|
|
|
private func toggleHDR() {
|
|
Task {
|
|
do {
|
|
let nextMode: CameraHDRMode
|
|
switch cameraSettings.hdrMode {
|
|
case .off: nextMode = .auto
|
|
case .auto: nextMode = .on
|
|
case .on: nextMode = .off
|
|
}
|
|
try setHDRMode(nextMode.toMijickHDRMode)
|
|
cameraSettings.hdrMode = nextMode
|
|
} catch {
|
|
print("Failed to set HDR mode: \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
private func toggleGrid() {
|
|
setGridVisibility(!isGridVisible)
|
|
}
|
|
|
|
private func flipCamera() {
|
|
Task {
|
|
do {
|
|
let newPosition: CameraPosition = (cameraPosition == .front) ? .back : .front
|
|
try await setCameraPosition(newPosition)
|
|
} catch {
|
|
print("Failed to flip camera: \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
private func toggleCenterStage() {
|
|
// Get the current camera device using AVFoundation
|
|
let deviceTypes: [AVCaptureDevice.DeviceType] = [
|
|
.builtInWideAngleCamera,
|
|
.builtInUltraWideCamera,
|
|
.builtInTelephotoCamera
|
|
]
|
|
|
|
guard let device = AVCaptureDevice.default(deviceTypes[0], for: .video, position: cameraPosition == .front ? .front : .back) else {
|
|
print("No camera device available for Center Stage toggle")
|
|
return
|
|
}
|
|
|
|
do {
|
|
// Configure Center Stage globally (these are static properties)
|
|
try device.lockForConfiguration()
|
|
|
|
// Set control mode to app-controlled
|
|
if device.activeFormat.isCenterStageSupported {
|
|
AVCaptureDevice.centerStageControlMode = .app
|
|
AVCaptureDevice.isCenterStageEnabled = !isCenterStageEnabled
|
|
}
|
|
|
|
device.unlockForConfiguration()
|
|
|
|
// Update our state
|
|
isCenterStageEnabled.toggle()
|
|
|
|
print("Center Stage toggled to: \(isCenterStageEnabled)")
|
|
} catch {
|
|
print("Failed to toggle Center Stage: \(error)")
|
|
}
|
|
}
|
|
|
|
private func cycleQuality() {
|
|
let allCases = PhotoQuality.allCases
|
|
let currentIndex = allCases.firstIndex(of: photoQuality) ?? 0
|
|
let nextIndex = (currentIndex + 1) % allCases.count
|
|
photoQuality = allCases[nextIndex]
|
|
}
|
|
|
|
private func toggleRingLight() {
|
|
isRingLightEnabled.toggle()
|
|
}
|
|
|
|
private func updateFlashSyncState() {
|
|
// Tell MijickCamera whether we're handling flash ourselves (sync enabled)
|
|
// or if iOS should handle it (sync disabled)
|
|
// We use .white as a placeholder - the actual color comes from ringLightColor in SwiftUI
|
|
if isFlashSyncedWithRingLight {
|
|
setScreenFlashColor(.white) // Non-nil = we handle flash, disable iOS flash
|
|
} else {
|
|
setScreenFlashColor(nil) // nil = let iOS handle Retina Flash
|
|
}
|
|
}
|
|
|
|
/// The color to use for screen flash overlay
|
|
private var screenFlashColor: Color {
|
|
isFlashSyncedWithRingLight ? ringLightColor : .white
|
|
}
|
|
|
|
/// Whether to use custom screen flash (front camera + flash on + sync enabled)
|
|
private var shouldUseCustomScreenFlash: Bool {
|
|
cameraPosition == .front && flashMode != .off && isFlashSyncedWithRingLight
|
|
}
|
|
|
|
/// Performs capture with screen flash if needed
|
|
private func performCapture() {
|
|
print("performCapture called - shouldUseCustomScreenFlash: \(shouldUseCustomScreenFlash)")
|
|
if shouldUseCustomScreenFlash {
|
|
// Save original brightness and boost to max
|
|
originalBrightness = UIScreen.main.brightness
|
|
UIScreen.main.brightness = 1.0
|
|
|
|
// Show flash overlay
|
|
isShowingScreenFlash = true
|
|
|
|
// Wait for camera to adjust to bright screen, then capture
|
|
Task { @MainActor in
|
|
try? await Task.sleep(for: .milliseconds(150))
|
|
print("Calling captureOutput() with custom flash")
|
|
captureOutput()
|
|
|
|
// Keep flash visible briefly after capture
|
|
try? await Task.sleep(for: .milliseconds(100))
|
|
isShowingScreenFlash = false
|
|
UIScreen.main.brightness = originalBrightness
|
|
}
|
|
} else {
|
|
// Normal capture (iOS handles Retina Flash for front camera if needed)
|
|
print("Calling captureOutput() without custom flash")
|
|
captureOutput()
|
|
}
|
|
}
|
|
|
|
private func toggleRingLightColorPicker() {
|
|
showRingLightColorPicker = true
|
|
showRingLightSizeSlider = false // Hide other overlay
|
|
isControlsExpanded = false // Collapse controls panel
|
|
}
|
|
|
|
private func toggleRingLightSizeSlider() {
|
|
showRingLightSizeSlider = true
|
|
showRingLightColorPicker = false // Hide other overlay
|
|
showRingLightOpacitySlider = false // Hide other overlay
|
|
isControlsExpanded = false // Collapse controls panel
|
|
}
|
|
|
|
private func toggleRingLightOpacitySlider() {
|
|
showRingLightOpacitySlider = true
|
|
showRingLightColorPicker = false // Hide other overlay
|
|
showRingLightSizeSlider = false // Hide other overlay
|
|
isControlsExpanded = false // Collapse controls panel
|
|
}
|
|
}
|
|
|
|
// MARK: - Color Picker Overlay
|
|
|
|
struct ColorPickerOverlay: View {
|
|
@Binding var selectedColor: Color
|
|
@Binding var isPresented: Bool
|
|
|
|
private let colors: [Color] = [
|
|
.white, .red, .orange, .yellow, .green, .blue, .purple, .pink,
|
|
.gray, .black, Color(red: 1.0, green: 0.5, blue: 0.0), // Coral
|
|
Color(red: 0.5, green: 1.0, blue: 0.5), // Mint
|
|
Color(red: 0.5, green: 0.5, blue: 1.0), // Periwinkle
|
|
Color(red: 1.0, green: 0.5, blue: 1.0), // Magenta
|
|
]
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Semi-transparent background
|
|
Color.black.opacity(Design.Opacity.medium)
|
|
.ignoresSafeArea()
|
|
|
|
// Color picker content
|
|
VStack(spacing: Design.Spacing.medium) {
|
|
// Header
|
|
Text("Ring Light Color")
|
|
.font(.system(size: 18, weight: .semibold))
|
|
.foregroundStyle(Color.white)
|
|
|
|
// Color grid
|
|
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), spacing: Design.Spacing.small) {
|
|
ForEach(colors, id: \.self) { color in
|
|
Circle()
|
|
.fill(color)
|
|
.frame(width: 50, height: 50)
|
|
.overlay(
|
|
Circle()
|
|
.stroke(Color.white.opacity(selectedColor == color ? 1.0 : 0.3), lineWidth: selectedColor == color ? 3 : 1)
|
|
)
|
|
.onTapGesture {
|
|
selectedColor = color
|
|
isPresented = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// Done button
|
|
Button("Done") {
|
|
isPresented = false
|
|
}
|
|
.font(.system(size: 16, weight: .medium))
|
|
.foregroundStyle(Color.white)
|
|
.padding(.vertical, Design.Spacing.small)
|
|
}
|
|
.padding(Design.Spacing.large)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
|
.fill(Color.black.opacity(Design.Opacity.strong))
|
|
)
|
|
.padding(.horizontal, Design.Spacing.large)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Opacity Slider Overlay
|
|
|
|
struct OpacitySliderOverlay: View {
|
|
@Binding var selectedOpacity: Double
|
|
@Binding var isPresented: Bool
|
|
|
|
private let minOpacity: Double = 0.1
|
|
private let maxOpacity: Double = 1.0
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Semi-transparent background
|
|
Color.black.opacity(Design.Opacity.medium)
|
|
.ignoresSafeArea()
|
|
|
|
// Opacity slider content
|
|
VStack(spacing: Design.Spacing.medium) {
|
|
// Header
|
|
Text("Ring Light Brightness")
|
|
.font(.system(size: 18, weight: .semibold))
|
|
.foregroundStyle(Color.white)
|
|
|
|
// Current opacity display as percentage
|
|
Text("\(Int(selectedOpacity * 100))%")
|
|
.font(.system(size: 24, weight: .bold))
|
|
.foregroundStyle(Color.white)
|
|
.frame(width: 80)
|
|
|
|
// Slider
|
|
Slider(value: $selectedOpacity, in: minOpacity...maxOpacity, step: 0.05)
|
|
.tint(Color.white)
|
|
.padding(.horizontal, Design.Spacing.medium)
|
|
|
|
// Opacity range labels
|
|
HStack {
|
|
Text("10%")
|
|
.font(.system(size: 14))
|
|
.foregroundStyle(Color.white.opacity(0.7))
|
|
Spacer()
|
|
Text("100%")
|
|
.font(.system(size: 14))
|
|
.foregroundStyle(Color.white.opacity(0.7))
|
|
}
|
|
.padding(.horizontal, Design.Spacing.medium)
|
|
|
|
// Done button
|
|
Button("Done") {
|
|
isPresented = false
|
|
}
|
|
.font(.system(size: 16, weight: .medium))
|
|
.foregroundStyle(Color.white)
|
|
.padding(.vertical, Design.Spacing.small)
|
|
}
|
|
.padding(Design.Spacing.large)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
|
.fill(Color.black.opacity(Design.Opacity.strong))
|
|
)
|
|
.padding(.horizontal, Design.Spacing.large)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Size Slider Overlay
|
|
|
|
struct SizeSliderOverlay: View {
|
|
@Binding var selectedSize: CGFloat
|
|
@Binding var isPresented: Bool
|
|
|
|
private let minSize: CGFloat = 50
|
|
private let maxSize: CGFloat = 100
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Semi-transparent background
|
|
Color.black.opacity(Design.Opacity.medium)
|
|
.ignoresSafeArea()
|
|
|
|
// Size slider content
|
|
VStack(spacing: Design.Spacing.medium) {
|
|
// Header
|
|
Text("Ring Light Size")
|
|
.font(.system(size: 18, weight: .semibold))
|
|
.foregroundStyle(Color.white)
|
|
|
|
// Current size display
|
|
Text("\(Int(selectedSize))")
|
|
.font(.system(size: 24, weight: .bold))
|
|
.foregroundStyle(Color.white)
|
|
.frame(width: 60)
|
|
|
|
// Slider
|
|
Slider(value: $selectedSize, in: minSize...maxSize, step: 5)
|
|
.tint(Color.white)
|
|
.padding(.horizontal, Design.Spacing.medium)
|
|
|
|
// Size range labels
|
|
HStack {
|
|
Text("\(Int(minSize))")
|
|
.font(.system(size: 14))
|
|
.foregroundStyle(Color.white.opacity(0.7))
|
|
Spacer()
|
|
Text("\(Int(maxSize))")
|
|
.font(.system(size: 14))
|
|
.foregroundStyle(Color.white.opacity(0.7))
|
|
}
|
|
.padding(.horizontal, Design.Spacing.medium)
|
|
|
|
// Done button
|
|
Button("Done") {
|
|
isPresented = false
|
|
}
|
|
.font(.system(size: 16, weight: .medium))
|
|
.foregroundStyle(Color.white)
|
|
.padding(.vertical, Design.Spacing.small)
|
|
}
|
|
.padding(Design.Spacing.large)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
|
.fill(Color.black.opacity(Design.Opacity.strong))
|
|
)
|
|
.padding(.horizontal, Design.Spacing.large)
|
|
}
|
|
}
|
|
}
|