Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
915088f180
commit
eedd709ef5
@ -2,47 +2,7 @@ import SwiftUI
|
||||
import MijickCamera
|
||||
import Bedrock
|
||||
|
||||
// MARK: - Camera Settings Observable
|
||||
/// Shared observable class for camera settings to prevent MCamera recreation on changes
|
||||
@Observable @MainActor
|
||||
final class CameraSettingsState {
|
||||
var photoQuality: PhotoQuality
|
||||
var isRingLightEnabled: Bool
|
||||
var ringLightColor: Color
|
||||
var ringLightSize: CGFloat
|
||||
var ringLightOpacity: Double
|
||||
var flashMode: CameraFlashMode
|
||||
var isFlashSyncedWithRingLight: Bool
|
||||
var hdrMode: CameraHDRMode
|
||||
var isGridVisible: Bool
|
||||
var cameraPosition: CameraPosition
|
||||
|
||||
init(settings: SettingsViewModel) {
|
||||
self.photoQuality = settings.photoQuality
|
||||
self.isRingLightEnabled = settings.isRingLightEnabled
|
||||
self.ringLightColor = settings.lightColor
|
||||
self.ringLightSize = settings.ringSize
|
||||
self.ringLightOpacity = settings.ringLightOpacity
|
||||
self.flashMode = settings.flashMode
|
||||
self.isFlashSyncedWithRingLight = settings.isFlashSyncedWithRingLight
|
||||
self.hdrMode = settings.hdrMode
|
||||
self.isGridVisible = settings.isGridVisible
|
||||
self.cameraPosition = settings.cameraPosition
|
||||
}
|
||||
|
||||
func update(from settings: SettingsViewModel) {
|
||||
self.photoQuality = settings.photoQuality
|
||||
self.isRingLightEnabled = settings.isRingLightEnabled
|
||||
self.ringLightColor = settings.lightColor
|
||||
self.ringLightSize = settings.ringSize
|
||||
self.ringLightOpacity = settings.ringLightOpacity
|
||||
self.flashMode = settings.flashMode
|
||||
self.isFlashSyncedWithRingLight = settings.isFlashSyncedWithRingLight
|
||||
self.hdrMode = settings.hdrMode
|
||||
self.isGridVisible = settings.isGridVisible
|
||||
self.cameraPosition = settings.cameraPosition
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
@State private var settings = SettingsViewModel()
|
||||
@ -55,8 +15,7 @@ struct ContentView: View {
|
||||
@State private var isSavingPhoto = false
|
||||
@State private var saveError: String?
|
||||
|
||||
/// Camera settings in a shared observable to prevent MCamera recreation
|
||||
@State private var cameraSettings: CameraSettingsState?
|
||||
/// Settings are managed by SettingsViewModel which is already @Observable
|
||||
|
||||
/// Unique key to force MCamera recreation after photo capture
|
||||
/// Incrementing this value creates a new camera session with fresh AVCapturePhotoOutput
|
||||
@ -67,7 +26,7 @@ struct ContentView: View {
|
||||
// Camera view - only recreates when sessionKey changes due to Equatable
|
||||
if !showPhotoReview {
|
||||
CameraContainerView(
|
||||
cameraSettings: cameraSettings ?? CameraSettingsState(settings: settings),
|
||||
settings: settings,
|
||||
sessionKey: cameraSessionKey,
|
||||
onImageCaptured: { image in
|
||||
capturedPhoto = CapturedPhoto(image: image, timestamp: Date())
|
||||
@ -120,29 +79,16 @@ struct ContentView: View {
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.animation(.easeInOut(duration: Design.Animation.quick), value: showPhotoReview)
|
||||
.onAppear {
|
||||
cameraSettings = CameraSettingsState(settings: settings)
|
||||
}
|
||||
.onChange(of: settings.photoQuality) { _, _ in updateCameraSettings() }
|
||||
.onChange(of: settings.isRingLightEnabled) { _, newValue in
|
||||
updateCameraSettings()
|
||||
if settings.isFlashSyncedWithRingLight {
|
||||
settings.flashMode = newValue ? .on : .off
|
||||
}
|
||||
}
|
||||
.onChange(of: settings.lightColor) { _, _ in updateCameraSettings() }
|
||||
.onChange(of: settings.ringSize) { _, _ in updateCameraSettings() }
|
||||
.onChange(of: settings.ringLightOpacity) { _, _ in updateCameraSettings() }
|
||||
.onChange(of: settings.flashMode) { _, _ in updateCameraSettings() }
|
||||
.onChange(of: settings.isFlashSyncedWithRingLight) { _, newValue in
|
||||
updateCameraSettings()
|
||||
if newValue {
|
||||
settings.flashMode = settings.isRingLightEnabled ? .on : .off
|
||||
}
|
||||
}
|
||||
.onChange(of: settings.hdrMode) { _, _ in updateCameraSettings() }
|
||||
.onChange(of: settings.isGridVisible) { _, _ in updateCameraSettings() }
|
||||
.onChange(of: settings.cameraPosition) { _, _ in updateCameraSettings() }
|
||||
.sheet(isPresented: $showSettings) {
|
||||
SettingsView(viewModel: settings, showPaywall: $showPaywall)
|
||||
}
|
||||
@ -151,10 +97,6 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func updateCameraSettings() {
|
||||
cameraSettings?.update(from: settings)
|
||||
}
|
||||
|
||||
/// Resets state and regenerates camera session key to create a fresh camera instance
|
||||
private func resetCameraForNextCapture() {
|
||||
capturedPhoto = nil
|
||||
@ -169,7 +111,7 @@ struct ContentView: View {
|
||||
isSavingPhoto = true
|
||||
saveError = nil
|
||||
|
||||
let quality = cameraSettings?.photoQuality ?? .high
|
||||
let quality = settings.photoQuality
|
||||
|
||||
Task {
|
||||
let result = await PhotoLibraryService.savePhotoToLibrary(image, quality: quality)
|
||||
@ -199,7 +141,7 @@ struct ContentView: View {
|
||||
// MARK: - Camera Container View
|
||||
/// Wrapper view for MCamera - only recreates when sessionKey changes
|
||||
struct CameraContainerView: View, Equatable {
|
||||
let cameraSettings: CameraSettingsState
|
||||
let settings: SettingsViewModel
|
||||
let sessionKey: UUID
|
||||
let onImageCaptured: (UIImage) -> Void
|
||||
|
||||
@ -216,12 +158,12 @@ struct CameraContainerView: View, Equatable {
|
||||
cameraManager: cameraManager,
|
||||
namespace: namespace,
|
||||
closeMCameraAction: closeAction,
|
||||
cameraSettings: cameraSettings,
|
||||
cameraSettings: settings,
|
||||
onPhotoCaptured: onImageCaptured
|
||||
)
|
||||
}
|
||||
.setCapturedMediaScreen(nil)
|
||||
.setCameraPosition(cameraSettings.cameraPosition)
|
||||
.setCameraPosition(settings.cameraPosition)
|
||||
.startSession()
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,40 +18,10 @@ struct CustomCameraScreen: MCameraScreen {
|
||||
let closeMCameraAction: () -> ()
|
||||
|
||||
/// Shared camera settings state - using Observable class prevents MCamera recreation
|
||||
var cameraSettings: CameraSettingsState
|
||||
var cameraSettings: SettingsViewModel
|
||||
|
||||
/// 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
|
||||
@ -98,9 +68,9 @@ struct CustomCameraScreen: MCameraScreen {
|
||||
// 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,
|
||||
color: cameraSettings.isRingLightEnabled ? cameraSettings.lightColor : .black,
|
||||
width: cameraSettings.isRingLightEnabled ? cameraSettings.ringSize : Design.CornerRadius.large,
|
||||
opacity: cameraSettings.isRingLightEnabled ? cameraSettings.ringLightOpacity : 1.0,
|
||||
cornerRadius: Design.CornerRadius.large
|
||||
)
|
||||
.allowsHitTesting(false) // Allow touches to pass through to camera view
|
||||
@ -119,31 +89,31 @@ struct CustomCameraScreen: MCameraScreen {
|
||||
isExpanded: $isControlsExpanded,
|
||||
hasActiveSettings: hasActiveSettings,
|
||||
activeSettingsIcons: activeSettingsIcons,
|
||||
flashMode: flashMode,
|
||||
flashMode: cameraSettings.flashMode,
|
||||
flashIcon: flashIcon,
|
||||
onFlashTap: toggleFlash,
|
||||
isFlashSyncedWithRingLight: isFlashSyncedWithRingLight,
|
||||
isFlashSyncedWithRingLight: cameraSettings.isFlashSyncedWithRingLight,
|
||||
onFlashSyncTap: toggleFlashSync,
|
||||
hdrMode: cameraSettings.hdrMode,
|
||||
hdrIcon: hdrIcon,
|
||||
onHDRTap: toggleHDR,
|
||||
isGridVisible: isGridVisible,
|
||||
isGridVisible: cameraSettings.isGridVisible,
|
||||
gridIcon: gridIcon,
|
||||
onGridTap: toggleGrid,
|
||||
photoQuality: photoQuality,
|
||||
photoQuality: cameraSettings.photoQuality,
|
||||
onQualityTap: cycleQuality,
|
||||
isCenterStageAvailable: isCenterStageAvailable,
|
||||
isCenterStageEnabled: isCenterStageEnabled,
|
||||
onCenterStageTap: toggleCenterStage,
|
||||
isFrontCamera: cameraPosition == .front,
|
||||
onFlipCameraTap: flipCamera,
|
||||
isRingLightEnabled: isRingLightEnabled,
|
||||
isRingLightEnabled: cameraSettings.isRingLightEnabled,
|
||||
onRingLightTap: toggleRingLight,
|
||||
ringLightColor: ringLightColor,
|
||||
ringLightColor: cameraSettings.lightColor,
|
||||
onRingLightColorTap: toggleRingLightColorPicker,
|
||||
ringLightSize: ringLightSize,
|
||||
ringLightSize: cameraSettings.ringSize,
|
||||
onRingLightSizeTap: toggleRingLightSizeSlider,
|
||||
ringLightOpacity: ringLightOpacity,
|
||||
ringLightOpacity: cameraSettings.ringLightOpacity,
|
||||
onRingLightOpacityTap: toggleRingLightOpacitySlider
|
||||
)
|
||||
.padding(.horizontal, Design.Spacing.large)
|
||||
@ -169,31 +139,31 @@ struct CustomCameraScreen: MCameraScreen {
|
||||
isExpanded: $isControlsExpanded,
|
||||
hasActiveSettings: hasActiveSettings,
|
||||
activeSettingsIcons: activeSettingsIcons,
|
||||
flashMode: flashMode,
|
||||
flashMode: cameraSettings.flashMode,
|
||||
flashIcon: flashIcon,
|
||||
onFlashTap: toggleFlash,
|
||||
isFlashSyncedWithRingLight: isFlashSyncedWithRingLight,
|
||||
isFlashSyncedWithRingLight: cameraSettings.isFlashSyncedWithRingLight,
|
||||
onFlashSyncTap: toggleFlashSync,
|
||||
hdrMode: cameraSettings.hdrMode,
|
||||
hdrIcon: hdrIcon,
|
||||
onHDRTap: toggleHDR,
|
||||
isGridVisible: isGridVisible,
|
||||
isGridVisible: cameraSettings.isGridVisible,
|
||||
gridIcon: gridIcon,
|
||||
onGridTap: toggleGrid,
|
||||
photoQuality: photoQuality,
|
||||
photoQuality: cameraSettings.photoQuality,
|
||||
onQualityTap: cycleQuality,
|
||||
isCenterStageAvailable: isCenterStageAvailable,
|
||||
isCenterStageEnabled: isCenterStageEnabled,
|
||||
onCenterStageTap: toggleCenterStage,
|
||||
isFrontCamera: cameraPosition == .front,
|
||||
onFlipCameraTap: flipCamera,
|
||||
isRingLightEnabled: isRingLightEnabled,
|
||||
isRingLightEnabled: cameraSettings.isRingLightEnabled,
|
||||
onRingLightTap: toggleRingLight,
|
||||
ringLightColor: ringLightColor,
|
||||
ringLightColor: cameraSettings.lightColor,
|
||||
onRingLightColorTap: toggleRingLightColorPicker,
|
||||
ringLightSize: ringLightSize,
|
||||
ringLightSize: cameraSettings.ringSize,
|
||||
onRingLightSizeTap: toggleRingLightSizeSlider,
|
||||
ringLightOpacity: ringLightOpacity,
|
||||
ringLightOpacity: cameraSettings.ringLightOpacity,
|
||||
onRingLightOpacityTap: toggleRingLightOpacitySlider
|
||||
)
|
||||
.padding(.horizontal, Design.Spacing.large)
|
||||
@ -222,8 +192,8 @@ struct CustomCameraScreen: MCameraScreen {
|
||||
if showRingLightColorPicker {
|
||||
ColorPickerOverlay(
|
||||
selectedColor: Binding(
|
||||
get: { cameraSettings.ringLightColor },
|
||||
set: { cameraSettings.ringLightColor = $0 }
|
||||
get: { cameraSettings.selectedLightColor.color },
|
||||
set: { cameraSettings.selectedLightColor = RingLightColor.custom(with: $0) }
|
||||
),
|
||||
isPresented: $showRingLightColorPicker
|
||||
)
|
||||
@ -234,8 +204,8 @@ struct CustomCameraScreen: MCameraScreen {
|
||||
if showRingLightSizeSlider {
|
||||
SizeSliderOverlay(
|
||||
selectedSize: Binding(
|
||||
get: { cameraSettings.ringLightSize },
|
||||
set: { cameraSettings.ringLightSize = $0 }
|
||||
get: { cameraSettings.ringSize },
|
||||
set: { cameraSettings.ringSize = $0 }
|
||||
),
|
||||
isPresented: $showRingLightSizeSlider
|
||||
)
|
||||
@ -290,7 +260,7 @@ struct CustomCameraScreen: MCameraScreen {
|
||||
// Initialize zoom gesture state
|
||||
lastMagnification = zoomFactor
|
||||
}
|
||||
.onChange(of: isFlashSyncedWithRingLight) { _, _ in
|
||||
.onChange(of: cameraSettings.isFlashSyncedWithRingLight) { _, _ in
|
||||
// Only update when sync setting changes, not on every color change
|
||||
updateFlashSyncState()
|
||||
}
|
||||
@ -309,22 +279,22 @@ struct CustomCameraScreen: MCameraScreen {
|
||||
|
||||
/// Returns true if any setting is in a non-default state
|
||||
private var hasActiveSettings: Bool {
|
||||
flashMode != .off || cameraSettings.hdrMode != .off || isGridVisible || isCenterStageEnabled || isRingLightEnabled
|
||||
cameraSettings.flashMode != .off || cameraSettings.hdrMode != .off || cameraSettings.isGridVisible || isCenterStageEnabled || cameraSettings.isRingLightEnabled
|
||||
}
|
||||
|
||||
/// Returns icons for currently active settings (for collapsed pill display)
|
||||
private var activeSettingsIcons: [String] {
|
||||
var icons: [String] = []
|
||||
if flashMode != .off {
|
||||
if cameraSettings.flashMode != .off {
|
||||
icons.append(flashIcon)
|
||||
}
|
||||
if cameraSettings.hdrMode != .off {
|
||||
icons.append(hdrIcon)
|
||||
}
|
||||
if isGridVisible {
|
||||
if cameraSettings.isGridVisible {
|
||||
icons.append(gridIcon)
|
||||
}
|
||||
if isRingLightEnabled {
|
||||
if cameraSettings.isRingLightEnabled {
|
||||
icons.append("circle.fill")
|
||||
}
|
||||
if isCenterStageEnabled {
|
||||
@ -335,7 +305,7 @@ struct CustomCameraScreen: MCameraScreen {
|
||||
|
||||
// MARK: - Control Icons
|
||||
private var flashIcon: String {
|
||||
flashMode.icon
|
||||
cameraSettings.flashMode.icon
|
||||
}
|
||||
|
||||
private var hdrIcon: String {
|
||||
@ -347,7 +317,7 @@ struct CustomCameraScreen: MCameraScreen {
|
||||
}
|
||||
|
||||
private var gridIcon: String {
|
||||
isGridVisible ? "grid" : "grid"
|
||||
cameraSettings.isGridVisible ? "grid" : "grid"
|
||||
}
|
||||
|
||||
private var isCenterStageAvailable: Bool {
|
||||
@ -360,18 +330,18 @@ struct CustomCameraScreen: MCameraScreen {
|
||||
// MARK: - Actions
|
||||
private func toggleFlash() {
|
||||
let nextMode: CameraFlashMode
|
||||
switch flashMode {
|
||||
switch cameraSettings.flashMode {
|
||||
case .off: nextMode = .auto
|
||||
case .auto: nextMode = .on
|
||||
case .on: nextMode = .off
|
||||
}
|
||||
flashMode = nextMode
|
||||
cameraSettings.flashMode = nextMode
|
||||
// Update MijickCamera's flash mode so it knows to use iOS Retina Flash
|
||||
setFlashMode(nextMode.toMijickFlashMode)
|
||||
}
|
||||
|
||||
private func toggleFlashSync() {
|
||||
isFlashSyncedWithRingLight.toggle()
|
||||
cameraSettings.isFlashSyncedWithRingLight.toggle()
|
||||
}
|
||||
|
||||
private func toggleHDR() {
|
||||
@ -392,7 +362,8 @@ struct CustomCameraScreen: MCameraScreen {
|
||||
}
|
||||
|
||||
private func toggleGrid() {
|
||||
setGridVisibility(!isGridVisible)
|
||||
cameraSettings.isGridVisible.toggle()
|
||||
setGridVisibility(cameraSettings.isGridVisible)
|
||||
}
|
||||
|
||||
private func flipCamera() {
|
||||
@ -400,6 +371,7 @@ struct CustomCameraScreen: MCameraScreen {
|
||||
do {
|
||||
let newPosition: CameraPosition = (cameraPosition == .front) ? .back : .front
|
||||
try await setCameraPosition(newPosition)
|
||||
cameraSettings.cameraPosition = newPosition
|
||||
} catch {
|
||||
print("Failed to flip camera: \(error)")
|
||||
}
|
||||
@ -442,34 +414,34 @@ struct CustomCameraScreen: MCameraScreen {
|
||||
|
||||
private func cycleQuality() {
|
||||
let allCases = PhotoQuality.allCases
|
||||
let currentIndex = allCases.firstIndex(of: photoQuality) ?? 0
|
||||
let currentIndex = allCases.firstIndex(of: cameraSettings.photoQuality) ?? 0
|
||||
let nextIndex = (currentIndex + 1) % allCases.count
|
||||
photoQuality = allCases[nextIndex]
|
||||
cameraSettings.photoQuality = allCases[nextIndex]
|
||||
}
|
||||
|
||||
private func toggleRingLight() {
|
||||
isRingLightEnabled.toggle()
|
||||
cameraSettings.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 {
|
||||
if cameraSettings.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
|
||||
cameraSettings.isFlashSyncedWithRingLight ? cameraSettings.lightColor : .white
|
||||
}
|
||||
|
||||
|
||||
/// Whether to use custom screen flash (front camera + flash on + sync enabled)
|
||||
private var shouldUseCustomScreenFlash: Bool {
|
||||
cameraPosition == .front && flashMode != .off && isFlashSyncedWithRingLight
|
||||
cameraPosition == .front && cameraSettings.flashMode != .off && cameraSettings.isFlashSyncedWithRingLight
|
||||
}
|
||||
|
||||
/// Performs capture with screen flash if needed
|
||||
|
||||
Loading…
Reference in New Issue
Block a user