Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-01-04 11:56:15 -06:00
parent cf95c4e816
commit fefb6cf363
7 changed files with 334 additions and 432 deletions

View File

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EA836ABE2F0ACE8A00077F87"
BuildableName = "SelfieCam.app"
BlueprintName = "SelfieCam"
ReferencedContainer = "container:SelfieCam.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EA836ACB2F0ACE8B00077F87"
BuildableName = "SelfieCamTests.xctest"
BlueprintName = "SelfieCamTests"
ReferencedContainer = "container:SelfieCam.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EA836AD52F0ACE8B00077F87"
BuildableName = "SelfieCamUITests.xctest"
BlueprintName = "SelfieCamUITests"
ReferencedContainer = "container:SelfieCam.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EA836ABE2F0ACE8A00077F87"
BuildableName = "SelfieCam.app"
BlueprintName = "SelfieCam"
ReferencedContainer = "container:SelfieCam.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "ENABLE_DEBUG_PREMIUM"
value = "1"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EA836ABE2F0ACE8A00077F87"
BuildableName = "SelfieCam.app"
BlueprintName = "SelfieCam"
ReferencedContainer = "container:SelfieCam.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -10,5 +10,23 @@
<integer>4</integer> <integer>4</integer>
</dict> </dict>
</dict> </dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>EA836ABE2F0ACE8A00077F87</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>EA836ACB2F0ACE8B00077F87</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>EA836AD52F0ACE8B00077F87</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict>
</dict> </dict>
</plist> </plist>

View File

@ -54,7 +54,7 @@ struct ContentView: View {
.transition(.opacity) .transition(.opacity)
} }
// Settings button overlay // Settings button overlay - positioned with safe area consideration
VStack { VStack {
HStack { HStack {
Spacer() Spacer()
@ -72,10 +72,11 @@ struct ContentView: View {
.accessibilityLabel("Settings") .accessibilityLabel("Settings")
} }
.padding(.horizontal, Design.Spacing.large) .padding(.horizontal, Design.Spacing.large)
.padding(.top, Design.Spacing.medium) .padding(.top, Design.Spacing.small) // Reduced from medium to account for safe area
Spacer() Spacer()
} }
.safeAreaInset(edge: .top) { Color.clear.frame(height: 0) } // Ensures proper safe area handling
} }
.ignoresSafeArea() .ignoresSafeArea()
.animation(.easeInOut(duration: Design.Animation.quick), value: showPhotoReview) .animation(.easeInOut(duration: Design.Animation.quick), value: showPhotoReview)

View File

@ -26,13 +26,7 @@ struct CustomCameraScreen: MCameraScreen {
// Center Stage state // Center Stage state
@State private var isCenterStageEnabled: Bool = AVCaptureDevice.isCenterStageEnabled @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 // Screen flash state for front camera
@State private var isShowingScreenFlash: Bool = false @State private var isShowingScreenFlash: Bool = false
@ -47,6 +41,7 @@ struct CustomCameraScreen: MCameraScreen {
// Camera preview with pinch gesture - Metal layer doesn't respect SwiftUI clipping // Camera preview with pinch gesture - Metal layer doesn't respect SwiftUI clipping
createCameraOutputView() createCameraOutputView()
.ignoresSafeArea() .ignoresSafeArea()
.scaleEffect(x: cameraSettings.isMirrorFlipped ? -1 : 1, y: 1) // Apply horizontal mirror flip
.gesture( .gesture(
MagnificationGesture() MagnificationGesture()
.updating($magnification) { currentState, gestureState, transaction in .updating($magnification) { currentState, gestureState, transaction in
@ -80,95 +75,17 @@ struct CustomCameraScreen: MCameraScreen {
let isLandscape = geometry.size.width > geometry.size.height let isLandscape = geometry.size.width > geometry.size.height
if isLandscape { if isLandscape {
// Landscape layout: full-width centered controls, capture button on left with zoom above // Landscape layout: capture button on left with zoom above
ZStack { VStack {
// Centered controls across entire screen Spacer()
VStack(spacing: 0) { CaptureButton(action: { performCapture() })
// Top controls area - expandable panel (centered) Spacer()
ExpandableControlsPanel(
isExpanded: $isControlsExpanded,
hasActiveSettings: hasActiveSettings,
activeSettingsIcons: activeSettingsIcons,
flashMode: cameraSettings.flashMode,
flashIcon: flashIcon,
onFlashTap: toggleFlash,
isFlashSyncedWithRingLight: cameraSettings.isFlashSyncedWithRingLight,
onFlashSyncTap: toggleFlashSync,
hdrMode: cameraSettings.hdrMode,
hdrIcon: hdrIcon,
onHDRTap: toggleHDR,
isGridVisible: cameraSettings.isGridVisible,
gridIcon: gridIcon,
onGridTap: toggleGrid,
photoQuality: cameraSettings.photoQuality,
onQualityTap: cycleQuality,
isCenterStageAvailable: isCenterStageAvailable,
isCenterStageEnabled: isCenterStageEnabled,
onCenterStageTap: toggleCenterStage,
isFrontCamera: cameraPosition == .front,
onFlipCameraTap: flipCamera,
isRingLightEnabled: cameraSettings.isRingLightEnabled,
onRingLightTap: toggleRingLight,
ringLightColor: cameraSettings.lightColor,
onRingLightColorTap: toggleRingLightColorPicker,
ringLightSize: cameraSettings.ringSize,
onRingLightSizeTap: toggleRingLightSizeSlider,
ringLightOpacity: cameraSettings.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)
} }
.padding(.leading, Design.Spacing.large)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
} else { } else {
// Portrait layout: controls on top, capture button at bottom // Portrait layout: capture button at bottom
VStack(spacing: 0) { VStack(spacing: 0) {
// Top controls area - expandable panel
ExpandableControlsPanel(
isExpanded: $isControlsExpanded,
hasActiveSettings: hasActiveSettings,
activeSettingsIcons: activeSettingsIcons,
flashMode: cameraSettings.flashMode,
flashIcon: flashIcon,
onFlashTap: toggleFlash,
isFlashSyncedWithRingLight: cameraSettings.isFlashSyncedWithRingLight,
onFlashSyncTap: toggleFlashSync,
hdrMode: cameraSettings.hdrMode,
hdrIcon: hdrIcon,
onHDRTap: toggleHDR,
isGridVisible: cameraSettings.isGridVisible,
gridIcon: gridIcon,
onGridTap: toggleGrid,
photoQuality: cameraSettings.photoQuality,
onQualityTap: cycleQuality,
isCenterStageAvailable: isCenterStageAvailable,
isCenterStageEnabled: isCenterStageEnabled,
onCenterStageTap: toggleCenterStage,
isFrontCamera: cameraPosition == .front,
onFlipCameraTap: flipCamera,
isRingLightEnabled: cameraSettings.isRingLightEnabled,
onRingLightTap: toggleRingLight,
ringLightColor: cameraSettings.lightColor,
onRingLightColorTap: toggleRingLightColorPicker,
ringLightSize: cameraSettings.ringSize,
onRingLightSizeTap: toggleRingLightSizeSlider,
ringLightOpacity: cameraSettings.ringLightOpacity,
onRingLightOpacityTap: toggleRingLightOpacitySlider
)
.padding(.horizontal, Design.Spacing.large)
.padding(.top, Design.Spacing.medium)
Spacer() Spacer()
// Bottom controls // Bottom controls
@ -188,41 +105,7 @@ struct CustomCameraScreen: MCameraScreen {
} }
} }
// Ring light color picker overlay
if showRingLightColorPicker {
ColorPickerOverlay(
selectedColor: Binding(
get: { cameraSettings.selectedLightColor.color },
set: { cameraSettings.selectedLightColor = RingLightColor.custom(with: $0) }
),
isPresented: $showRingLightColorPicker
)
.transition(.opacity)
}
// Ring light size slider overlay
if showRingLightSizeSlider {
SizeSliderOverlay(
selectedSize: Binding(
get: { cameraSettings.ringSize },
set: { cameraSettings.ringSize = $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 // Screen flash overlay for front camera
if isShowingScreenFlash { if isShowingScreenFlash {
@ -232,26 +115,6 @@ struct CustomCameraScreen: MCameraScreen {
} }
} }
.animation(.easeInOut(duration: 0.05), value: isShowingScreenFlash) .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 { .onAppear {
// Set flash mode from saved settings // Set flash mode from saved settings
setFlashMode(cameraSettings.flashMode.toMijickFlashMode) setFlashMode(cameraSettings.flashMode.toMijickFlashMode)
@ -275,153 +138,9 @@ struct CustomCameraScreen: MCameraScreen {
} }
} }
// MARK: - Active Settings Detection
/// Returns true if any setting is in a non-default state
private var hasActiveSettings: Bool {
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 cameraSettings.flashMode != .off {
icons.append(flashIcon)
}
if cameraSettings.hdrMode != .off {
icons.append(hdrIcon)
}
if cameraSettings.isGridVisible {
icons.append(gridIcon)
}
if cameraSettings.isRingLightEnabled {
icons.append("circle.fill")
}
if isCenterStageEnabled {
icons.append("person.crop.rectangle.fill")
}
return icons
}
// MARK: - Control Icons
private var flashIcon: String {
cameraSettings.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 {
cameraSettings.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 // MARK: - Actions
private func toggleFlash() {
let nextMode: CameraFlashMode
switch cameraSettings.flashMode {
case .off: nextMode = .auto
case .auto: nextMode = .on
case .on: nextMode = .off
}
cameraSettings.flashMode = nextMode
// Update MijickCamera's flash mode so it knows to use iOS Retina Flash
setFlashMode(nextMode.toMijickFlashMode)
}
private func toggleFlashSync() {
cameraSettings.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() {
cameraSettings.isGridVisible.toggle()
setGridVisibility(cameraSettings.isGridVisible)
}
private func flipCamera() {
Task {
do {
let newPosition: CameraPosition = (cameraPosition == .front) ? .back : .front
try await setCameraPosition(newPosition)
cameraSettings.cameraPosition = 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: cameraSettings.photoQuality) ?? 0
let nextIndex = (currentIndex + 1) % allCases.count
cameraSettings.photoQuality = allCases[nextIndex]
}
private func toggleRingLight() {
cameraSettings.isRingLightEnabled.toggle()
}
private func updateFlashSyncState() { private func updateFlashSyncState() {
// Tell MijickCamera whether we're handling flash ourselves (sync enabled) // Tell MijickCamera whether we're handling flash ourselves (sync enabled)
@ -443,7 +162,7 @@ struct CustomCameraScreen: MCameraScreen {
private var shouldUseCustomScreenFlash: Bool { private var shouldUseCustomScreenFlash: Bool {
cameraPosition == .front && cameraSettings.flashMode != .off && cameraSettings.isFlashSyncedWithRingLight cameraPosition == .front && cameraSettings.flashMode != .off && cameraSettings.isFlashSyncedWithRingLight
} }
/// Performs capture with screen flash if needed /// Performs capture with screen flash if needed
private func performCapture() { private func performCapture() {
print("performCapture called - shouldUseCustomScreenFlash: \(shouldUseCustomScreenFlash)") print("performCapture called - shouldUseCustomScreenFlash: \(shouldUseCustomScreenFlash)")
@ -451,16 +170,16 @@ struct CustomCameraScreen: MCameraScreen {
// Save original brightness and boost to max // Save original brightness and boost to max
originalBrightness = UIScreen.main.brightness originalBrightness = UIScreen.main.brightness
UIScreen.main.brightness = 1.0 UIScreen.main.brightness = 1.0
// Show flash overlay // Show flash overlay
isShowingScreenFlash = true isShowingScreenFlash = true
// Wait for camera to adjust to bright screen, then capture // Wait for camera to adjust to bright screen, then capture
Task { @MainActor in Task { @MainActor in
try? await Task.sleep(for: .milliseconds(150)) try? await Task.sleep(for: .milliseconds(150))
print("Calling captureOutput() with custom flash") print("Calling captureOutput() with custom flash")
captureOutput() captureOutput()
// Keep flash visible briefly after capture // Keep flash visible briefly after capture
try? await Task.sleep(for: .milliseconds(100)) try? await Task.sleep(for: .milliseconds(100))
isShowingScreenFlash = false isShowingScreenFlash = false
@ -472,24 +191,4 @@ struct CustomCameraScreen: MCameraScreen {
captureOutput() 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
}
} }

View File

@ -14,8 +14,8 @@ struct SizeSliderOverlay: View {
@Binding var selectedSize: CGFloat @Binding var selectedSize: CGFloat
@Binding var isPresented: Bool @Binding var isPresented: Bool
private let minSize: CGFloat = 50 private let minSize: CGFloat = SettingsViewModel.minRingSize
private let maxSize: CGFloat = 100 private let maxSize: CGFloat = SettingsViewModel.maxRingSize
var body: some View { var body: some View {
ZStack { ZStack {

View File

@ -17,48 +17,34 @@ struct SettingsView: View {
NavigationStack { NavigationStack {
ScrollView { ScrollView {
VStack(spacing: Design.Spacing.medium) { VStack(spacing: Design.Spacing.medium) {
// MARK: - Ring Light Section // MARK: - Ring Light Section
SettingsSectionHeader(title: "Ring Light", systemImage: "light.max") SettingsSectionHeader(title: "Ring Light", systemImage: "light.max")
// Ring Light Enabled
SettingsToggle(
title: String(localized: "Enable Ring Light"),
subtitle: String(localized: "Show colored light ring around camera preview"),
isOn: $viewModel.isRingLightEnabled
)
.accessibilityHint(String(localized: "Enables or disables the ring light overlay"))
// Ring Size Slider // Ring Size Slider
ringSizeSlider ringSizeSlider
// Color Preset // Color Preset
colorPresetSection colorPresetSection
// MARK: - Camera Section // Ring Light Brightness
ringLightBrightnessSlider
SettingsSectionHeader(title: "Camera", systemImage: "camera")
// MARK: - Camera Controls Section
SettingsToggle(
title: String(localized: "Front Flash"), SettingsSectionHeader(title: "Camera Controls", systemImage: "camera")
subtitle: String(localized: "Hides preview during capture for a flash effect"),
isOn: $viewModel.isFrontFlashEnabled // Camera Position
) cameraPositionPicker
.accessibilityHint(String(localized: "Uses the ring light as a flash when taking photos"))
SettingsToggle(
title: String(localized: "True Mirror"),
subtitle: String(localized: "Shows non-flipped preview like a real mirror"),
isOn: $viewModel.isMirrorFlipped
)
.accessibilityHint(String(localized: "When enabled, the preview is not mirrored"))
SettingsToggle(
title: String(localized: "Skin Smoothing"),
subtitle: String(localized: "Applies subtle real-time smoothing"),
isOn: $viewModel.isSkinSmoothingEnabled
)
.accessibilityHint(String(localized: "Applies light skin smoothing to the camera preview"))
SettingsToggle(
title: String(localized: "Grid Overlay"),
subtitle: String(localized: "Shows rule of thirds grid"),
isOn: $viewModel.isGridVisible
)
.accessibilityHint(String(localized: "Shows a grid overlay to help compose your shot"))
// Flash Mode // Flash Mode
flashModePicker flashModePicker
@ -66,61 +52,80 @@ struct SettingsView: View {
// Flash Sync // Flash Sync
SettingsToggle( SettingsToggle(
title: String(localized: "Flash Sync"), title: String(localized: "Flash Sync"),
subtitle: String(localized: "Use ring light color for flash"), subtitle: String(localized: "Use ring light color for screen flash"),
isOn: $viewModel.isFlashSyncedWithRingLight isOn: $viewModel.isFlashSyncedWithRingLight
) )
.accessibilityHint(String(localized: "Syncs flash color with ring light color")) .accessibilityHint(String(localized: "Syncs flash color with ring light color"))
// Front Flash
SettingsToggle(
title: String(localized: "Front Flash"),
subtitle: String(localized: "Hide preview during capture for flash effect"),
isOn: $viewModel.isFrontFlashEnabled
)
.accessibilityHint(String(localized: "Uses screen flash when taking front camera photos"))
// HDR Mode // HDR Mode
hdrModePicker hdrModePicker
// Photo Quality // Photo Quality
photoQualityPicker photoQualityPicker
// Camera Position // MARK: - Display Section
cameraPositionPicker
SettingsSectionHeader(title: "Display", systemImage: "eye")
// Ring Light Enabled
SettingsToggle( SettingsToggle(
title: String(localized: "Ring Light Enabled"), title: String(localized: "True Mirror"),
subtitle: String(localized: "Show ring light around camera"), subtitle: String(localized: "Shows horizontally flipped preview like a real mirror"),
isOn: $viewModel.isRingLightEnabled isOn: $viewModel.isMirrorFlipped
) )
.accessibilityHint(String(localized: "Enables or disables the ring light overlay")) .accessibilityHint(String(localized: "Flips the camera preview horizontally"))
// Ring Light Brightness SettingsToggle(
ringLightBrightnessSlider title: String(localized: "Grid Overlay"),
subtitle: String(localized: "Shows rule of thirds grid for composition"),
isOn: $viewModel.isGridVisible
)
.accessibilityHint(String(localized: "Shows a grid overlay to help compose your shot"))
SettingsToggle(
title: String(localized: "Skin Smoothing"),
subtitle: String(localized: "Applies subtle real-time skin smoothing"),
isOn: $viewModel.isSkinSmoothingEnabled
)
.accessibilityHint(String(localized: "Applies light skin smoothing to the camera preview"))
// MARK: - Capture Section
SettingsSectionHeader(title: "Capture", systemImage: "photo.on.rectangle")
// Timer Selection // Timer Selection
timerPicker timerPicker
// MARK: - Capture Section
SettingsSectionHeader(title: "Capture", systemImage: "photo.on.rectangle")
SettingsToggle( SettingsToggle(
title: String(localized: "Auto-Save"), title: String(localized: "Auto-Save"),
subtitle: String(localized: "Automatically save captures to Photo Library"), subtitle: String(localized: "Automatically save captures to Photo Library"),
isOn: $viewModel.isAutoSaveEnabled isOn: $viewModel.isAutoSaveEnabled
) )
.accessibilityHint(String(localized: "When enabled, photos and videos are saved immediately after capture")) .accessibilityHint(String(localized: "When enabled, photos and videos are saved immediately after capture"))
// MARK: - Pro Section // MARK: - Pro Section
SettingsSectionHeader(title: "Pro", systemImage: "crown") SettingsSectionHeader(title: "Pro", systemImage: "crown")
proSection proSection
// MARK: - Sync Section // MARK: - Sync Section
SettingsSectionHeader(title: "iCloud Sync", systemImage: "icloud") SettingsSectionHeader(title: "iCloud Sync", systemImage: "icloud")
iCloudSyncSection iCloudSyncSection
// MARK: - About Section // MARK: - About Section
SettingsSectionHeader(title: "About", systemImage: "info.circle") SettingsSectionHeader(title: "About", systemImage: "info.circle")
acknowledgmentsSection acknowledgmentsSection
Spacer(minLength: Design.Spacing.xxxLarge) Spacer(minLength: Design.Spacing.xxxLarge)
@ -239,48 +244,88 @@ struct SettingsView: View {
// MARK: - Flash Mode Picker // MARK: - Flash Mode Picker
private var flashModePicker: some View { private var flashModePicker: some View {
SegmentedPicker( VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
title: String(localized: "Flash Mode"), Text(String(localized: "Flash Mode"))
options: CameraFlashMode.allCases.map { ($0.displayName, $0) }, .font(.system(size: Design.BaseFontSize.medium, weight: .medium))
selection: $viewModel.flashMode .foregroundStyle(.white)
)
.accessibilityLabel(String(localized: "Select flash mode")) Text(String(localized: "Controls automatic flash behavior for photos"))
.font(.system(size: Design.BaseFontSize.caption))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
SegmentedPicker(
title: "",
options: CameraFlashMode.allCases.map { ($0.displayName, $0) },
selection: $viewModel.flashMode
)
.accessibilityLabel(String(localized: "Select flash mode"))
}
} }
// MARK: - HDR Mode Picker // MARK: - HDR Mode Picker
private var hdrModePicker: some View { private var hdrModePicker: some View {
SegmentedPicker( VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
title: String(localized: "HDR Mode"), Text(String(localized: "HDR Mode"))
options: CameraHDRMode.allCases.map { ($0.displayName, $0) }, .font(.system(size: Design.BaseFontSize.medium, weight: .medium))
selection: $viewModel.hdrMode .foregroundStyle(.white)
)
.accessibilityLabel(String(localized: "Select HDR mode")) Text(String(localized: "High Dynamic Range for better lighting in photos"))
.font(.system(size: Design.BaseFontSize.caption))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
SegmentedPicker(
title: "",
options: CameraHDRMode.allCases.map { ($0.displayName, $0) },
selection: $viewModel.hdrMode
)
.accessibilityLabel(String(localized: "Select HDR mode"))
}
} }
// MARK: - Photo Quality Picker // MARK: - Photo Quality Picker
private var photoQualityPicker: some View { private var photoQualityPicker: some View {
SegmentedPicker( VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
title: String(localized: "Photo Quality"), Text(String(localized: "Photo Quality"))
options: PhotoQuality.allCases.map { ($0.rawValue.capitalized, $0) }, .font(.system(size: Design.BaseFontSize.medium, weight: .medium))
selection: $viewModel.photoQuality .foregroundStyle(.white)
)
.accessibilityLabel(String(localized: "Select photo quality")) Text(String(localized: "File size and image quality for saved photos"))
.font(.system(size: Design.BaseFontSize.caption))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
SegmentedPicker(
title: "",
options: PhotoQuality.allCases.map { ($0.rawValue.capitalized, $0) },
selection: $viewModel.photoQuality
)
.accessibilityLabel(String(localized: "Select photo quality"))
}
} }
// MARK: - Camera Position Picker // MARK: - Camera Position Picker
private var cameraPositionPicker: some View { private var cameraPositionPicker: some View {
SegmentedPicker( VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
title: String(localized: "Camera"), Text(String(localized: "Camera"))
options: [ .font(.system(size: Design.BaseFontSize.medium, weight: .medium))
(String(localized: "Front"), CameraPosition.front), .foregroundStyle(.white)
(String(localized: "Back"), CameraPosition.back)
], Text(String(localized: "Choose between front and back camera lenses"))
selection: $viewModel.cameraPosition .font(.system(size: Design.BaseFontSize.caption))
) .foregroundStyle(.white.opacity(Design.Opacity.medium))
.accessibilityLabel(String(localized: "Select camera position"))
SegmentedPicker(
title: "",
options: [
(String(localized: "Front"), CameraPosition.front),
(String(localized: "Back"), CameraPosition.back)
],
selection: $viewModel.cameraPosition
)
.accessibilityLabel(String(localized: "Select camera position"))
}
} }
// MARK: - Ring Light Brightness Slider // MARK: - Ring Light Brightness Slider
@ -328,12 +373,22 @@ struct SettingsView: View {
// MARK: - Timer Picker // MARK: - Timer Picker
private var timerPicker: some View { private var timerPicker: some View {
SegmentedPicker( VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
title: String(localized: "Self-Timer"), Text(String(localized: "Self-Timer"))
options: TimerOption.allCases.map { ($0.displayName, $0) }, .font(.system(size: Design.BaseFontSize.medium, weight: .medium))
selection: $viewModel.selectedTimer .foregroundStyle(.white)
)
.accessibilityLabel(String(localized: "Select self-timer duration")) Text(String(localized: "Delay before photo capture for self-portraits"))
.font(.system(size: Design.BaseFontSize.caption))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
SegmentedPicker(
title: "",
options: TimerOption.allCases.map { ($0.displayName, $0) },
selection: $viewModel.selectedTimer
)
.accessibilityLabel(String(localized: "Select self-timer duration"))
}
} }
// MARK: - Pro Section // MARK: - Pro Section

View File

@ -65,7 +65,7 @@
"comment" : "A hint for the \"Skin Smoothing\" toggle in the settings view.", "comment" : "A hint for the \"Skin Smoothing\" toggle in the settings view.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Applies subtle real-time smoothing" : { "Applies subtle real-time skin smoothing" : {
"comment" : "Accessibility hint for the \"Skin Smoothing\" toggle in the Settings view.", "comment" : "Accessibility hint for the \"Skin Smoothing\" toggle in the Settings view.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
@ -117,11 +117,19 @@
}, },
"Center Stage active" : { "Center Stage active" : {
},
"Choose between front and back camera lenses" : {
"comment" : "A description of the camera position picker.",
"isCommentAutoGenerated" : true
}, },
"Close preview" : { "Close preview" : {
"comment" : "A button label that closes the preview screen.", "comment" : "A button label that closes the preview screen.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Controls automatic flash behavior for photos" : {
"comment" : "A description below the flash mode picker, explaining its purpose.",
"isCommentAutoGenerated" : true
},
"Cool Lavender" : { "Cool Lavender" : {
"comment" : "Name of a ring light color preset.", "comment" : "Name of a ring light color preset.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -142,6 +150,10 @@
"comment" : "Accessibility announcement when restoring purchases in debug mode.", "comment" : "Accessibility announcement when restoring purchases in debug mode.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Delay before photo capture for self-portraits" : {
"comment" : "A description of the purpose of the \"Self-Timer\" setting in the settings screen.",
"isCommentAutoGenerated" : true
},
"Directional Gradient Lighting" : { "Directional Gradient Lighting" : {
"comment" : "Benefit provided with the Pro subscription, such as \"Directional Gradient Lighting\".", "comment" : "Benefit provided with the Pro subscription, such as \"Directional Gradient Lighting\".",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -154,10 +166,18 @@
"comment" : "An accessibility hint for the capture button, instructing the user to double-tap it to capture a photo.", "comment" : "An accessibility hint for the capture button, instructing the user to double-tap it to capture a photo.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Enable Ring Light" : {
"comment" : "Title of a toggle in the Settings view that allows the user to enable or disable the ring light overlay.",
"isCommentAutoGenerated" : true
},
"Enables or disables the ring light overlay" : { "Enables or disables the ring light overlay" : {
"comment" : "A toggle that enables or disables the ring light overlay.", "comment" : "A toggle that enables or disables the ring light overlay.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"File size and image quality for saved photos" : {
"comment" : "A description of the photo quality setting.",
"isCommentAutoGenerated" : true
},
"Flash Mode" : { "Flash Mode" : {
"comment" : "Title of a segmented picker that allows the user to select the flash mode of the camera.", "comment" : "Title of a segmented picker that allows the user to select the flash mode of the camera.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -166,6 +186,10 @@
"comment" : "Title of a toggle that synchronizes the flash color with the ring light color.", "comment" : "Title of a toggle that synchronizes the flash color with the ring light color.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Flips the camera preview horizontally" : {
"comment" : "An accessibility hint for the \"True Mirror\" setting.",
"isCommentAutoGenerated" : true
},
"Front" : { "Front" : {
"comment" : "Option in the camera position picker for using the front camera.", "comment" : "Option in the camera position picker for using the front camera.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -186,8 +210,12 @@
"comment" : "Title for a picker that allows the user to select the HDR mode of the camera.", "comment" : "Title for a picker that allows the user to select the HDR mode of the camera.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Hides preview during capture for a flash effect" : { "Hide preview during capture for flash effect" : {
"comment" : "Subtitle for the \"Front Flash\" toggle in the Settings view.", "comment" : "Text displayed in a toggle within the \"Camera Controls\" section, allowing the user to enable or disable the feature of hiding the camera preview during a photo capture to simulate a flash effect.",
"isCommentAutoGenerated" : true
},
"High Dynamic Range for better lighting in photos" : {
"comment" : "A description of the High Dynamic Range (HDR) mode in the settings view.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Ice Blue" : { "Ice Blue" : {
@ -275,10 +303,6 @@
"comment" : "The title of the color picker overlay.", "comment" : "The title of the color picker overlay.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Ring Light Enabled" : {
"comment" : "Title of a toggle that enables or disables the ring light overlay.",
"isCommentAutoGenerated" : true
},
"Ring Light Size" : { "Ring Light Size" : {
"comment" : "The title of the slider that allows the user to select the size of their ring light.", "comment" : "The title of the slider that allows the user to select the size of their ring light.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -335,20 +359,20 @@
"comment" : "Title for a button that shares the captured media.", "comment" : "Title for a button that shares the captured media.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Show ring light around camera" : { "Show colored light ring around camera preview" : {
"comment" : "Title of a toggle that enables or disables the ring light overlay.", "comment" : "Subtitle for the \"Enable Ring Light\" toggle in the Settings view.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Shows a grid overlay to help compose your shot" : { "Shows a grid overlay to help compose your shot" : {
"comment" : "A toggle that enables or disables the rule of thirds grid overlay in the camera view.", "comment" : "A toggle that enables or disables the rule of thirds grid overlay in the camera view.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Shows non-flipped preview like a real mirror" : { "Shows horizontally flipped preview like a real mirror" : {
"comment" : "Subtitle for the \"True Mirror\" toggle in the Settings view.", "comment" : "Description of a setting that flips the camera preview horizontally.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Shows rule of thirds grid" : { "Shows rule of thirds grid for composition" : {
"comment" : "Accessibility hint for the grid overlay toggle.", "comment" : "A toggle that enables or disables the display of a rule of thirds grid on the camera preview.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Sign in to iCloud to enable sync" : { "Sign in to iCloud to enable sync" : {
@ -428,16 +452,16 @@
"comment" : "A button label that prompts users to upgrade to the premium version of the app.", "comment" : "A button label that prompts users to upgrade to the premium version of the app.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Use ring light color for flash" : { "Use ring light color for screen flash" : {
"comment" : "Text for the \"Flash Sync\" toggle in the Settings view.", "comment" : "Accessibility hint for the \"Flash Sync\" toggle in the Settings view.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Use the buttons at the bottom to save or share your photo" : { "Use the buttons at the bottom to save or share your photo" : {
"comment" : "An accessibility hint for the photo review view, instructing the user on how to interact with the view.", "comment" : "An accessibility hint for the photo review view, instructing the user on how to interact with the view.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Uses the ring light as a flash when taking photos" : { "Uses screen flash when taking front camera photos" : {
"comment" : "An accessibility hint for the \"Front Flash\" toggle in the Settings view.", "comment" : "A toggle that enables or disables the use of the front camera's flash during photo captures.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Video" : { "Video" : {
@ -460,10 +484,6 @@
"comment" : "A hint provided by the \"Auto-Save\" toggle in the Settings view, explaining that photos and videos are saved immediately after capture when enabled.", "comment" : "A hint provided by the \"Auto-Save\" toggle in the Settings view, explaining that photos and videos are saved immediately after capture when enabled.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"When enabled, the preview is not mirrored" : {
"comment" : "Accessibility hint for the \"True Mirror\" setting in the Settings view.",
"isCommentAutoGenerated" : true
},
"Zoom %@ times" : { "Zoom %@ times" : {
"comment" : "A label describing the zoom level of the camera view. The argument is the string “%.1f”.", "comment" : "A label describing the zoom level of the camera view. The argument is the string “%.1f”.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true