Add branded SelfieCamTheme with visual improvements to settings
- Create SelfieCamTheme.swift with rose/magenta-tinted surface colors matching app branding - Add App-prefixed typealiases (AppSurface, AppAccent, AppStatus, etc.) to avoid Bedrock conflicts - Add SettingsCard container for visual grouping of related settings - Update SettingsView with card containers and accent-colored section headers - Update all settings-related files to use new theme colors - Pro section uses warning color, Debug section uses error color for visual distinction
This commit is contained in:
parent
66736d39f9
commit
255f3c2d68
@ -302,6 +302,61 @@ The codebase is structured for future extraction into reusable packages:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Known Issues / TODO
|
||||||
|
|
||||||
|
### Camera Control Button Light Press - NOT WORKING
|
||||||
|
|
||||||
|
**Status:** ❌ Broken - Needs Investigation
|
||||||
|
|
||||||
|
The Camera Control button (iPhone 16+) **full press works** for photo capture, but the **light press (secondary action) does NOT work**.
|
||||||
|
|
||||||
|
Testing revealed that the "secondary" events in logs were actually triggered by **volume button**, not Camera Control light press. The volume button works because `onCameraCaptureEvent` handles all hardware capture buttons.
|
||||||
|
|
||||||
|
#### What Works:
|
||||||
|
- ✅ Camera Control full press → triggers photo capture
|
||||||
|
- ✅ Volume up/down → triggers secondary event (focus lock)
|
||||||
|
|
||||||
|
#### What Doesn't Work:
|
||||||
|
- ❌ Camera Control light press → no event received at all
|
||||||
|
- ❌ Camera Control swipe gestures (zoom) → Apple-exclusive API
|
||||||
|
|
||||||
|
#### User Action Required - Check Accessibility Settings:
|
||||||
|
|
||||||
|
**Settings > Accessibility > Camera Control**:
|
||||||
|
- Ensure **Camera Control** is enabled
|
||||||
|
- Ensure **Light-Press** is turned ON
|
||||||
|
- Adjust **Light-Press Force** if needed
|
||||||
|
- Check **Double Light-Press Speed**
|
||||||
|
|
||||||
|
These system settings may affect third-party apps differently than Apple Camera.
|
||||||
|
|
||||||
|
#### Investigation Areas:
|
||||||
|
|
||||||
|
1. **Accessibility settings may block third-party light press**
|
||||||
|
- User reports light press works in Apple Camera but not SelfieCam
|
||||||
|
- System may require explicit light-press enablement per-app
|
||||||
|
|
||||||
|
2. **MijickCamera session configuration**
|
||||||
|
- The third-party camera framework may interfere with light press detection
|
||||||
|
- MijickCamera manages its own AVCaptureSession - may conflict
|
||||||
|
- Try testing with raw AVCaptureSession to isolate the issue
|
||||||
|
|
||||||
|
3. **`onCameraCaptureEvent` secondaryAction limitations**
|
||||||
|
- The `secondaryAction` closure receives volume button events correctly
|
||||||
|
- Camera Control light press may use different event pathway
|
||||||
|
- Apple may internally route light press to their Camera app exclusively
|
||||||
|
|
||||||
|
4. **Light press may require AVCapturePhotoOutput configuration**
|
||||||
|
- Secondary events might need specific photo output settings
|
||||||
|
- Check if `AVCapturePhotoSettings` has light-press related properties
|
||||||
|
|
||||||
|
5. **Possible Apple restriction (most likely)**
|
||||||
|
- Light press and swipe gestures appear restricted to first-party apps
|
||||||
|
- Similar to swipe-to-zoom which is Apple-exclusive
|
||||||
|
- No public API documentation confirms light press availability
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Future Enhancements
|
## Future Enhancements
|
||||||
|
|
||||||
Potential areas for expansion:
|
Potential areas for expansion:
|
||||||
@ -312,6 +367,7 @@ Potential areas for expansion:
|
|||||||
- [ ] Apple Watch remote trigger
|
- [ ] Apple Watch remote trigger
|
||||||
- [ ] Export presets (aspect ratios, watermarks)
|
- [ ] Export presets (aspect ratios, watermarks)
|
||||||
- [ ] Social sharing integrations
|
- [ ] Social sharing integrations
|
||||||
|
- [ ] Camera Control button swipe-to-zoom (if Apple makes API public)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
// CaptureEventInteraction.swift
|
// CaptureEventInteraction.swift
|
||||||
// SelfieCam
|
// SelfieCam
|
||||||
//
|
//
|
||||||
// Handles hardware capture events using AVCaptureEventInteraction (iOS 17.2+).
|
// Handles hardware capture events using AVCaptureEventInteraction.
|
||||||
// Supports Camera Control button (iPhone 16+), Action button, and volume buttons.
|
// Supports Camera Control button (iPhone 16+), Action button, and volume buttons.
|
||||||
//
|
//
|
||||||
|
|
||||||
@ -18,18 +18,20 @@ import UIKit
|
|||||||
/// - **Camera Control button** (iPhone 16+): Full press to capture, light press for focus/exposure
|
/// - **Camera Control button** (iPhone 16+): Full press to capture, light press for focus/exposure
|
||||||
/// - **Action button** (iPhone 15 Pro+): When configured for camera
|
/// - **Action button** (iPhone 15 Pro+): When configured for camera
|
||||||
/// - **Volume buttons**: Hardware shutter via system integration
|
/// - **Volume buttons**: Hardware shutter via system integration
|
||||||
///
|
|
||||||
/// - Note: Requires iOS 17.2+ for `AVCaptureEventInteraction` API
|
|
||||||
@Observable @MainActor
|
@Observable @MainActor
|
||||||
final class CaptureEventManager {
|
final class CaptureEventManager {
|
||||||
private(set) var isActive = false
|
private(set) var isActive = false
|
||||||
private(set) var isFocusLocked = false
|
private(set) var isFocusLocked = false
|
||||||
|
|
||||||
// Store as Any to avoid availability issues with the type declaration
|
private var eventInteraction: AVCaptureEventInteraction?
|
||||||
private var eventInteraction: AnyObject?
|
|
||||||
private var onCapture: (() -> Void)?
|
private var onCapture: (() -> Void)?
|
||||||
private var onFocusLock: ((Bool) -> Void)?
|
private var onFocusLock: ((Bool) -> Void)?
|
||||||
|
|
||||||
|
/// Pre-initialized haptic generators for immediate response
|
||||||
|
private let lightHaptic = UIImpactFeedbackGenerator(style: .light)
|
||||||
|
private let mediumHaptic = UIImpactFeedbackGenerator(style: .medium)
|
||||||
|
private let heavyHaptic = UIImpactFeedbackGenerator(style: .heavy)
|
||||||
|
|
||||||
/// Sets up capture event handling with the provided callbacks
|
/// Sets up capture event handling with the provided callbacks
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - onCapture: Called when a capture event is triggered (full press)
|
/// - onCapture: Called when a capture event is triggered (full press)
|
||||||
@ -42,19 +44,10 @@ final class CaptureEventManager {
|
|||||||
self.onFocusLock = onFocusLock
|
self.onFocusLock = onFocusLock
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates and returns the UIInteraction to be added to a view
|
/// Creates and returns the AVCaptureEventInteraction to be added to a view
|
||||||
/// - Returns: The configured interaction, or nil if not available
|
func createInteraction() -> AVCaptureEventInteraction {
|
||||||
func createInteraction() -> (any UIInteraction)? {
|
Design.debugLog("📸 [CameraControl] Creating AVCaptureEventInteraction...")
|
||||||
guard #available(iOS 17.2, *) else {
|
|
||||||
Design.debugLog("CaptureEventInteraction: iOS 17.2+ required")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return createCaptureEventInteraction()
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS 17.2, *)
|
|
||||||
private func createCaptureEventInteraction() -> AVCaptureEventInteraction {
|
|
||||||
let interaction = AVCaptureEventInteraction(
|
let interaction = AVCaptureEventInteraction(
|
||||||
primary: { [weak self] event in
|
primary: { [weak self] event in
|
||||||
Task { @MainActor [weak self] in
|
Task { @MainActor [weak self] in
|
||||||
@ -68,9 +61,16 @@ final class CaptureEventManager {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Must explicitly enable the interaction
|
||||||
|
interaction.isEnabled = true
|
||||||
|
|
||||||
|
// Prepare haptic engines for immediate response
|
||||||
|
prepareHaptics()
|
||||||
|
|
||||||
self.eventInteraction = interaction
|
self.eventInteraction = interaction
|
||||||
self.isActive = true
|
self.isActive = true
|
||||||
Design.debugLog("CaptureEventInteraction: Hardware capture events enabled")
|
Design.debugLog("📸 [CameraControl] ✅ Hardware capture events ENABLED")
|
||||||
|
Design.debugLog("📸 [CameraControl] Supported: Full press (capture), Light press (focus lock)")
|
||||||
|
|
||||||
return interaction
|
return interaction
|
||||||
}
|
}
|
||||||
@ -80,156 +80,82 @@ final class CaptureEventManager {
|
|||||||
eventInteraction = nil
|
eventInteraction = nil
|
||||||
isActive = false
|
isActive = false
|
||||||
isFocusLocked = false
|
isFocusLocked = false
|
||||||
Design.debugLog("CaptureEventInteraction: Hardware capture events disabled")
|
Design.debugLog("📸 [CameraControl] Hardware capture events disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Event Handlers
|
// MARK: - Event Handlers
|
||||||
|
|
||||||
@available(iOS 17.2, *)
|
|
||||||
private func handlePrimaryEvent(_ event: AVCaptureEvent) {
|
private func handlePrimaryEvent(_ event: AVCaptureEvent) {
|
||||||
|
Design.debugLog("📸 [CameraControl] PRIMARY - phase: \(event.phase.rawValue)")
|
||||||
|
|
||||||
switch event.phase {
|
switch event.phase {
|
||||||
case .began:
|
case .began:
|
||||||
Design.debugLog("CaptureEventInteraction: Primary event began (press detected)")
|
Design.debugLog("📸 [CameraControl] PRIMARY BEGAN - Full press started")
|
||||||
// Haptic feedback on press
|
triggerHaptic(.medium)
|
||||||
triggerHaptic(.light)
|
|
||||||
|
|
||||||
case .ended:
|
case .ended:
|
||||||
Design.debugLog("CaptureEventInteraction: Primary event ended (capture)")
|
Design.debugLog("📸 [CameraControl] PRIMARY ENDED - Triggering capture!")
|
||||||
|
triggerHaptic(.heavy)
|
||||||
onCapture?()
|
onCapture?()
|
||||||
|
|
||||||
case .cancelled:
|
case .cancelled:
|
||||||
Design.debugLog("CaptureEventInteraction: Primary event cancelled")
|
Design.debugLog("📸 [CameraControl] PRIMARY CANCELLED")
|
||||||
|
|
||||||
@unknown default:
|
@unknown default:
|
||||||
break
|
Design.debugLog("📸 [CameraControl] PRIMARY UNKNOWN phase: \(event.phase.rawValue)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 17.2, *)
|
|
||||||
private func handleSecondaryEvent(_ event: AVCaptureEvent) {
|
private func handleSecondaryEvent(_ event: AVCaptureEvent) {
|
||||||
|
Design.debugLog("🔒 [CameraControl] SECONDARY - phase: \(event.phase.rawValue)")
|
||||||
|
|
||||||
switch event.phase {
|
switch event.phase {
|
||||||
case .began:
|
case .began:
|
||||||
Design.debugLog("CaptureEventInteraction: Secondary event began (focus lock)")
|
Design.debugLog("🔒 [CameraControl] SECONDARY BEGAN - Light press! Locking focus...")
|
||||||
isFocusLocked = true
|
isFocusLocked = true
|
||||||
onFocusLock?(true)
|
onFocusLock?(true)
|
||||||
triggerHaptic(.light)
|
triggerHaptic(.medium)
|
||||||
|
|
||||||
case .ended:
|
case .ended:
|
||||||
Design.debugLog("CaptureEventInteraction: Secondary event ended (focus unlock)")
|
Design.debugLog("🔒 [CameraControl] SECONDARY ENDED - Unlocking focus")
|
||||||
isFocusLocked = false
|
isFocusLocked = false
|
||||||
onFocusLock?(false)
|
onFocusLock?(false)
|
||||||
|
triggerHaptic(.light)
|
||||||
|
|
||||||
case .cancelled:
|
case .cancelled:
|
||||||
Design.debugLog("CaptureEventInteraction: Secondary event cancelled")
|
Design.debugLog("🔒 [CameraControl] SECONDARY CANCELLED")
|
||||||
isFocusLocked = false
|
isFocusLocked = false
|
||||||
onFocusLock?(false)
|
onFocusLock?(false)
|
||||||
|
|
||||||
@unknown default:
|
@unknown default:
|
||||||
break
|
Design.debugLog("🔒 [CameraControl] SECONDARY UNKNOWN phase: \(event.phase.rawValue)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Haptic Feedback
|
// MARK: - Haptic Feedback
|
||||||
|
|
||||||
|
/// Prepares haptic engines for low-latency feedback
|
||||||
|
func prepareHaptics() {
|
||||||
|
lightHaptic.prepare()
|
||||||
|
mediumHaptic.prepare()
|
||||||
|
heavyHaptic.prepare()
|
||||||
|
}
|
||||||
|
|
||||||
private func triggerHaptic(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
|
private func triggerHaptic(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
|
||||||
let generator = UIImpactFeedbackGenerator(style: style)
|
switch style {
|
||||||
generator.impactOccurred()
|
case .light:
|
||||||
}
|
lightHaptic.impactOccurred()
|
||||||
}
|
lightHaptic.prepare()
|
||||||
|
case .medium:
|
||||||
// MARK: - SwiftUI View for Hosting the Interaction
|
mediumHaptic.impactOccurred()
|
||||||
|
mediumHaptic.prepare()
|
||||||
/// A SwiftUI view that hosts AVCaptureEventInteraction for Camera Control button support
|
case .heavy:
|
||||||
///
|
heavyHaptic.impactOccurred()
|
||||||
/// Add this view to your camera hierarchy to enable hardware capture events.
|
heavyHaptic.prepare()
|
||||||
/// The view is invisible and passes through all touches.
|
default:
|
||||||
///
|
let generator = UIImpactFeedbackGenerator(style: style)
|
||||||
/// Usage:
|
generator.impactOccurred()
|
||||||
/// ```swift
|
|
||||||
/// ZStack {
|
|
||||||
/// CameraPreview()
|
|
||||||
/// CaptureEventInteractionView(
|
|
||||||
/// onCapture: { performCapture() },
|
|
||||||
/// onFocusLock: { locked in handleFocusLock(locked) }
|
|
||||||
/// )
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
struct CaptureEventInteractionView: UIViewRepresentable {
|
|
||||||
/// Called when primary capture event occurs (full press)
|
|
||||||
let onCapture: () -> Void
|
|
||||||
|
|
||||||
/// Called when focus/exposure lock state changes (light press)
|
|
||||||
var onFocusLock: ((Bool) -> Void)?
|
|
||||||
|
|
||||||
func makeUIView(context: Context) -> CaptureEventHostView {
|
|
||||||
let view = CaptureEventHostView()
|
|
||||||
view.backgroundColor = .clear
|
|
||||||
view.isUserInteractionEnabled = true
|
|
||||||
|
|
||||||
// Configure and add the interaction
|
|
||||||
context.coordinator.manager.configure(
|
|
||||||
onCapture: onCapture,
|
|
||||||
onFocusLock: onFocusLock
|
|
||||||
)
|
|
||||||
|
|
||||||
if let interaction = context.coordinator.manager.createInteraction() {
|
|
||||||
view.addInteraction(interaction)
|
|
||||||
context.coordinator.interaction = interaction
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIView(_ uiView: CaptureEventHostView, context: Context) {
|
|
||||||
// Update callbacks if they change
|
|
||||||
context.coordinator.manager.configure(
|
|
||||||
onCapture: onCapture,
|
|
||||||
onFocusLock: onFocusLock
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeCoordinator() -> Coordinator {
|
|
||||||
Coordinator()
|
|
||||||
}
|
|
||||||
|
|
||||||
static func dismantleUIView(_ uiView: CaptureEventHostView, coordinator: Coordinator) {
|
|
||||||
if let interaction = coordinator.interaction {
|
|
||||||
uiView.removeInteraction(interaction)
|
|
||||||
}
|
|
||||||
coordinator.manager.invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
class Coordinator {
|
|
||||||
let manager = CaptureEventManager()
|
|
||||||
var interaction: (any UIInteraction)?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// UIView subclass that hosts AVCaptureEventInteraction
|
|
||||||
///
|
|
||||||
/// This view is transparent and allows touches to pass through while
|
|
||||||
/// still receiving hardware capture events.
|
|
||||||
final class CaptureEventHostView: UIView {
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
super.init(frame: frame)
|
|
||||||
setup()
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
super.init(coder: coder)
|
|
||||||
setup()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setup() {
|
|
||||||
backgroundColor = .clear
|
|
||||||
isUserInteractionEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow touches to pass through to underlying views
|
|
||||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
||||||
// Return nil to pass touches through, but the interaction still receives hardware events
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,27 +163,27 @@ final class CaptureEventHostView: UIView {
|
|||||||
|
|
||||||
extension CaptureEventManager {
|
extension CaptureEventManager {
|
||||||
/// Checks if the device supports Camera Control button
|
/// Checks if the device supports Camera Control button
|
||||||
|
/// Note: The API is always available on iOS 18+, but the physical button is iPhone 16+ only
|
||||||
static var isCameraControlSupported: Bool {
|
static var isCameraControlSupported: Bool {
|
||||||
guard #available(iOS 17.2, *) else { return false }
|
|
||||||
|
|
||||||
// Camera Control is available on iPhone 16 series
|
|
||||||
// We check for iOS 17.2+ as the API requirement
|
|
||||||
// The actual button is hardware-dependent
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a description of supported hardware capture methods
|
/// Returns a description of supported hardware capture methods
|
||||||
static var supportedHardwareMethods: String {
|
static var supportedHardwareMethods: String {
|
||||||
var methods: [String] = []
|
return "Camera Control (iPhone 16+), Action Button, Volume Buttons"
|
||||||
|
}
|
||||||
if #available(iOS 17.2, *) {
|
}
|
||||||
methods.append("Camera Control (iPhone 16+)")
|
|
||||||
methods.append("Action Button")
|
// MARK: - AVCaptureEventPhase Extension
|
||||||
methods.append("Volume Buttons")
|
|
||||||
} else {
|
extension AVCaptureEventPhase {
|
||||||
methods.append("Volume Buttons (legacy)")
|
/// Converts AVCaptureEventPhase to our CaptureEventPhase enum
|
||||||
}
|
var toCaptureEventPhase: CaptureEventPhase {
|
||||||
|
switch self {
|
||||||
return methods.joined(separator: ", ")
|
case .began: return .began
|
||||||
|
case .ended: return .ended
|
||||||
|
case .cancelled: return .cancelled
|
||||||
|
@unknown default: return .cancelled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
import AVKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Bedrock
|
import Bedrock
|
||||||
import MijickCamera
|
import MijickCamera
|
||||||
@ -52,6 +53,18 @@ struct CustomCameraScreen: MCameraScreen {
|
|||||||
createCameraOutputView()
|
createCameraOutputView()
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.scaleEffect(x: cameraSettings.isMirrorFlipped ? -1 : 1, y: 1) // Apply horizontal mirror flip
|
.scaleEffect(x: cameraSettings.isMirrorFlipped ? -1 : 1, y: 1) // Apply horizontal mirror flip
|
||||||
|
// Camera Control button - applied directly to camera preview for best event handling
|
||||||
|
.onCameraCaptureEvent(
|
||||||
|
isEnabled: true,
|
||||||
|
primaryAction: { event in
|
||||||
|
Design.debugLog("📸 [onCameraCaptureEvent] primaryAction received!")
|
||||||
|
handlePrimaryCaptureEvent(event)
|
||||||
|
},
|
||||||
|
secondaryAction: { event in
|
||||||
|
Design.debugLog("🔒 [onCameraCaptureEvent] secondaryAction received!")
|
||||||
|
handleSecondaryCaptureEvent(event)
|
||||||
|
}
|
||||||
|
)
|
||||||
// Pinch to zoom
|
// Pinch to zoom
|
||||||
.gesture(
|
.gesture(
|
||||||
MagnificationGesture()
|
MagnificationGesture()
|
||||||
@ -167,16 +180,6 @@ struct CustomCameraScreen: MCameraScreen {
|
|||||||
// Hidden volume view to prevent system HUD when using volume buttons as shutter
|
// Hidden volume view to prevent system HUD when using volume buttons as shutter
|
||||||
HiddenVolumeView()
|
HiddenVolumeView()
|
||||||
.frame(width: 0, height: 0)
|
.frame(width: 0, height: 0)
|
||||||
|
|
||||||
// Camera Control button and hardware capture event support (iOS 17.2+)
|
|
||||||
CaptureEventInteractionView(
|
|
||||||
onCapture: { performCapture() },
|
|
||||||
onFocusLock: { locked in
|
|
||||||
handleFocusLock(locked)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
.allowsHitTesting(false)
|
|
||||||
}
|
}
|
||||||
.animation(.easeInOut(duration: 0.05), value: isShowingScreenFlash)
|
.animation(.easeInOut(duration: 0.05), value: isShowingScreenFlash)
|
||||||
.animation(.easeInOut(duration: 0.3), value: isCountdownActive)
|
.animation(.easeInOut(duration: 0.3), value: isCountdownActive)
|
||||||
@ -422,16 +425,69 @@ struct CustomCameraScreen: MCameraScreen {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Camera Control Button Handlers
|
||||||
|
|
||||||
|
/// Handles PRIMARY Camera Control event (full press - capture photo)
|
||||||
|
private func handlePrimaryCaptureEvent(_ event: AVCaptureEvent) {
|
||||||
|
Design.debugLog("📸 [CameraControl] PRIMARY - phase: \(event.phase.rawValue)")
|
||||||
|
|
||||||
|
switch event.phase {
|
||||||
|
case .began:
|
||||||
|
Design.debugLog("📸 [CameraControl] PRIMARY BEGAN - Press detected")
|
||||||
|
triggerHaptic(.medium)
|
||||||
|
|
||||||
|
case .ended:
|
||||||
|
Design.debugLog("📸 [CameraControl] PRIMARY ENDED - Capturing photo!")
|
||||||
|
triggerHaptic(.heavy)
|
||||||
|
performCapture()
|
||||||
|
|
||||||
|
case .cancelled:
|
||||||
|
Design.debugLog("📸 [CameraControl] PRIMARY CANCELLED")
|
||||||
|
|
||||||
|
@unknown default:
|
||||||
|
Design.debugLog("📸 [CameraControl] PRIMARY UNKNOWN phase")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles SECONDARY Camera Control event (light press - focus/exposure lock)
|
||||||
|
private func handleSecondaryCaptureEvent(_ event: AVCaptureEvent) {
|
||||||
|
Design.debugLog("🔒 [CameraControl] SECONDARY - phase: \(event.phase.rawValue)")
|
||||||
|
|
||||||
|
switch event.phase {
|
||||||
|
case .began:
|
||||||
|
Design.debugLog("🔒 [CameraControl] SECONDARY BEGAN - Light press! Locking focus...")
|
||||||
|
triggerHaptic(.medium)
|
||||||
|
handleFocusLock(true)
|
||||||
|
|
||||||
|
case .ended:
|
||||||
|
Design.debugLog("🔒 [CameraControl] SECONDARY ENDED - Unlocking focus")
|
||||||
|
triggerHaptic(.light)
|
||||||
|
handleFocusLock(false)
|
||||||
|
|
||||||
|
case .cancelled:
|
||||||
|
Design.debugLog("🔒 [CameraControl] SECONDARY CANCELLED")
|
||||||
|
handleFocusLock(false)
|
||||||
|
|
||||||
|
@unknown default:
|
||||||
|
Design.debugLog("🔒 [CameraControl] SECONDARY UNKNOWN phase")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Focus/Exposure Lock (Camera Control Light Press)
|
// MARK: - Focus/Exposure Lock (Camera Control Light Press)
|
||||||
|
|
||||||
/// Handles focus/exposure lock from Camera Control button light press
|
/// Handles focus/exposure lock from Camera Control button light press
|
||||||
/// - Parameter locked: Whether focus/exposure is being locked
|
/// - Parameter locked: Whether focus/exposure is being locked
|
||||||
private func handleFocusLock(_ locked: Bool) {
|
private func handleFocusLock(_ locked: Bool) {
|
||||||
|
Design.debugLog("🔒 [FocusLock] handleFocusLock called with locked=\(locked)")
|
||||||
|
|
||||||
guard let device = AVCaptureDevice.default(
|
guard let device = AVCaptureDevice.default(
|
||||||
.builtInWideAngleCamera,
|
.builtInWideAngleCamera,
|
||||||
for: .video,
|
for: .video,
|
||||||
position: cameraPosition == .front ? .front : .back
|
position: cameraPosition == .front ? .front : .back
|
||||||
) else { return }
|
) else {
|
||||||
|
Design.debugLog("🔒 [FocusLock] ⚠️ No capture device found!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try device.lockForConfiguration()
|
try device.lockForConfiguration()
|
||||||
@ -440,25 +496,31 @@ struct CustomCameraScreen: MCameraScreen {
|
|||||||
// Lock current focus and exposure
|
// Lock current focus and exposure
|
||||||
if device.isFocusModeSupported(.locked) {
|
if device.isFocusModeSupported(.locked) {
|
||||||
device.focusMode = .locked
|
device.focusMode = .locked
|
||||||
|
Design.debugLog("🔒 [FocusLock] ✅ Focus LOCKED")
|
||||||
|
} else {
|
||||||
|
Design.debugLog("🔒 [FocusLock] ⚠️ Focus lock not supported")
|
||||||
}
|
}
|
||||||
if device.isExposureModeSupported(.locked) {
|
if device.isExposureModeSupported(.locked) {
|
||||||
device.exposureMode = .locked
|
device.exposureMode = .locked
|
||||||
|
Design.debugLog("🔒 [FocusLock] ✅ Exposure LOCKED")
|
||||||
|
} else {
|
||||||
|
Design.debugLog("🔒 [FocusLock] ⚠️ Exposure lock not supported")
|
||||||
}
|
}
|
||||||
Design.debugLog("Focus/Exposure locked via Camera Control")
|
|
||||||
} else {
|
} else {
|
||||||
// Restore continuous autofocus and auto exposure
|
// Restore continuous autofocus and auto exposure
|
||||||
if device.isFocusModeSupported(.continuousAutoFocus) {
|
if device.isFocusModeSupported(.continuousAutoFocus) {
|
||||||
device.focusMode = .continuousAutoFocus
|
device.focusMode = .continuousAutoFocus
|
||||||
|
Design.debugLog("🔒 [FocusLock] ✅ Focus UNLOCKED (continuous)")
|
||||||
}
|
}
|
||||||
if device.isExposureModeSupported(.continuousAutoExposure) {
|
if device.isExposureModeSupported(.continuousAutoExposure) {
|
||||||
device.exposureMode = .continuousAutoExposure
|
device.exposureMode = .continuousAutoExposure
|
||||||
|
Design.debugLog("🔒 [FocusLock] ✅ Exposure UNLOCKED (continuous)")
|
||||||
}
|
}
|
||||||
Design.debugLog("Focus/Exposure unlocked via Camera Control")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
device.unlockForConfiguration()
|
device.unlockForConfiguration()
|
||||||
} catch {
|
} catch {
|
||||||
Design.debugLog("Failed to set focus/exposure lock: \(error)")
|
Design.debugLog("🔒 [FocusLock] ❌ Error: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -62,7 +62,7 @@ struct ProPaywallView: View {
|
|||||||
}
|
}
|
||||||
.padding(Design.Spacing.large)
|
.padding(Design.Spacing.large)
|
||||||
}
|
}
|
||||||
.background(Color.Surface.overlay)
|
.background(AppSurface.overlay)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .cancellationAction) {
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
@ -102,11 +102,11 @@ private struct ProductPackageButton: View {
|
|||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(Design.Spacing.large)
|
.padding(Design.Spacing.large)
|
||||||
.background(Color.Accent.primary.opacity(Design.Opacity.medium))
|
.background(AppAccent.primary.opacity(Design.Opacity.medium))
|
||||||
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
|
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
|
||||||
.overlay(
|
.overlay(
|
||||||
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
||||||
.strokeBorder(Color.Accent.primary, lineWidth: Design.LineWidth.thin)
|
.strokeBorder(AppAccent.primary, lineWidth: Design.LineWidth.thin)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.accessibilityLabel(String(localized: "Subscribe to \(package.storeProduct.localizedTitle) for \(package.localizedPriceString)"))
|
.accessibilityLabel(String(localized: "Subscribe to \(package.storeProduct.localizedTitle) for \(package.localizedPriceString)"))
|
||||||
@ -123,7 +123,7 @@ struct BenefitRow: View {
|
|||||||
HStack(spacing: Design.Spacing.medium) {
|
HStack(spacing: Design.Spacing.medium) {
|
||||||
Image(systemName: image)
|
Image(systemName: image)
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.foregroundStyle(Color.Accent.primary)
|
.foregroundStyle(AppAccent.primary)
|
||||||
.frame(width: Design.IconSize.xLarge)
|
.frame(width: Design.IconSize.xLarge)
|
||||||
|
|
||||||
Text(text)
|
Text(text)
|
||||||
|
|||||||
@ -31,7 +31,7 @@ struct ColorPresetButton: View {
|
|||||||
.overlay(
|
.overlay(
|
||||||
Circle()
|
Circle()
|
||||||
.strokeBorder(
|
.strokeBorder(
|
||||||
isSelected ? Color.Accent.primary : Color.Border.subtle,
|
isSelected ? AppAccent.primary : AppBorder.subtle,
|
||||||
lineWidth: isSelected ? Design.LineWidth.thick : Design.LineWidth.thin
|
lineWidth: isSelected ? Design.LineWidth.thick : Design.LineWidth.thin
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -61,13 +61,13 @@ struct ColorPresetButton: View {
|
|||||||
if preset.isPremium {
|
if preset.isPremium {
|
||||||
Image(systemName: isPremiumUnlocked ? "crown.fill" : "crown")
|
Image(systemName: isPremiumUnlocked ? "crown.fill" : "crown")
|
||||||
.font(.system(size: Design.BaseFontSize.xxSmall))
|
.font(.system(size: Design.BaseFontSize.xxSmall))
|
||||||
.foregroundStyle(isPremiumUnlocked ? Color.Status.warning : Color.Status.warning.opacity(Design.Opacity.medium))
|
.foregroundStyle(isPremiumUnlocked ? AppStatus.warning : AppStatus.warning.opacity(Design.Opacity.medium))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(Design.Spacing.xSmall)
|
.padding(Design.Spacing.xSmall)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
|
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
|
||||||
.fill(isSelected ? Color.Accent.primary.opacity(Design.Opacity.subtle) : Color.clear)
|
.fill(isSelected ? AppAccent.primary.opacity(Design.Opacity.subtle) : Color.clear)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
|||||||
@ -36,7 +36,7 @@ struct CustomColorPickerButton: View {
|
|||||||
.overlay(
|
.overlay(
|
||||||
Circle()
|
Circle()
|
||||||
.strokeBorder(
|
.strokeBorder(
|
||||||
isSelected ? Color.Accent.primary : Color.Border.subtle,
|
isSelected ? AppAccent.primary : AppBorder.subtle,
|
||||||
lineWidth: isSelected ? Design.LineWidth.thick : Design.LineWidth.thin
|
lineWidth: isSelected ? Design.LineWidth.thick : Design.LineWidth.thin
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -53,12 +53,12 @@ struct CustomColorPickerButton: View {
|
|||||||
|
|
||||||
Image(systemName: "crown.fill")
|
Image(systemName: "crown.fill")
|
||||||
.font(.system(size: Design.BaseFontSize.xxSmall))
|
.font(.system(size: Design.BaseFontSize.xxSmall))
|
||||||
.foregroundStyle(Color.Status.warning)
|
.foregroundStyle(AppStatus.warning)
|
||||||
}
|
}
|
||||||
.padding(Design.Spacing.xSmall)
|
.padding(Design.Spacing.xSmall)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
|
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
|
||||||
.fill(isSelected ? Color.Accent.primary.opacity(Design.Opacity.subtle) : Color.clear)
|
.fill(isSelected ? AppAccent.primary.opacity(Design.Opacity.subtle) : Color.clear)
|
||||||
)
|
)
|
||||||
.accessibilityLabel(String(localized: "Custom color"))
|
.accessibilityLabel(String(localized: "Custom color"))
|
||||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
||||||
@ -78,7 +78,7 @@ struct CustomColorPickerButton: View {
|
|||||||
.frame(width: Design.Size.avatarSmall, height: Design.Size.avatarSmall)
|
.frame(width: Design.Size.avatarSmall, height: Design.Size.avatarSmall)
|
||||||
.overlay(
|
.overlay(
|
||||||
Circle()
|
Circle()
|
||||||
.strokeBorder(Color.Border.subtle, lineWidth: Design.LineWidth.thin)
|
.strokeBorder(AppBorder.subtle, lineWidth: Design.LineWidth.thin)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Lock overlay
|
// Lock overlay
|
||||||
@ -99,7 +99,7 @@ struct CustomColorPickerButton: View {
|
|||||||
|
|
||||||
Image(systemName: "crown")
|
Image(systemName: "crown")
|
||||||
.font(.system(size: Design.BaseFontSize.xxSmall))
|
.font(.system(size: Design.BaseFontSize.xxSmall))
|
||||||
.foregroundStyle(Color.Status.warning.opacity(Design.Opacity.medium))
|
.foregroundStyle(AppStatus.warning.opacity(Design.Opacity.medium))
|
||||||
}
|
}
|
||||||
.padding(Design.Spacing.xSmall)
|
.padding(Design.Spacing.xSmall)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,7 +32,7 @@ struct LicensesView: View {
|
|||||||
}
|
}
|
||||||
.padding(Design.Spacing.large)
|
.padding(Design.Spacing.large)
|
||||||
}
|
}
|
||||||
.background(Color.Surface.overlay)
|
.background(AppSurface.overlay)
|
||||||
.navigationTitle(String(localized: "Open Source Licenses"))
|
.navigationTitle(String(localized: "Open Source Licenses"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
}
|
}
|
||||||
@ -50,7 +50,7 @@ struct LicensesView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Label(license, systemImage: "doc.text")
|
Label(license, systemImage: "doc.text")
|
||||||
.font(.system(size: Design.BaseFontSize.xSmall))
|
.font(.system(size: Design.BaseFontSize.xSmall))
|
||||||
.foregroundStyle(Color.Accent.primary)
|
.foregroundStyle(AppAccent.primary)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@ -58,13 +58,13 @@ struct LicensesView: View {
|
|||||||
Link(destination: linkURL) {
|
Link(destination: linkURL) {
|
||||||
Label(String(localized: "View on GitHub"), systemImage: "arrow.up.right.square")
|
Label(String(localized: "View on GitHub"), systemImage: "arrow.up.right.square")
|
||||||
.font(.system(size: Design.BaseFontSize.xSmall))
|
.font(.system(size: Design.BaseFontSize.xSmall))
|
||||||
.foregroundStyle(Color.Accent.primary)
|
.foregroundStyle(AppAccent.primary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.top, Design.Spacing.xSmall)
|
.padding(.top, Design.Spacing.xSmall)
|
||||||
}
|
}
|
||||||
.padding(Design.Spacing.medium)
|
.padding(Design.Spacing.medium)
|
||||||
.background(Color.Surface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
|
.background(AppSurface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,124 +28,139 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
// MARK: - Ring Light Section
|
// MARK: - Ring Light Section
|
||||||
|
|
||||||
SettingsSectionHeader(title: "Ring Light", systemImage: "light.max")
|
SettingsSectionHeader(title: "Ring Light", systemImage: "light.max", accentColor: AppAccent.primary)
|
||||||
|
|
||||||
// Ring Light Enabled
|
SettingsCard {
|
||||||
SettingsToggle(
|
// Ring Light Enabled
|
||||||
title: String(localized: "Enable Ring Light"),
|
SettingsToggle(
|
||||||
subtitle: String(localized: "Show colored light ring around camera preview"),
|
title: String(localized: "Enable Ring Light"),
|
||||||
isOn: $viewModel.isRingLightEnabled
|
subtitle: String(localized: "Show colored light ring around camera preview"),
|
||||||
)
|
isOn: $viewModel.isRingLightEnabled,
|
||||||
.accessibilityHint(String(localized: "Enables or disables the ring light overlay"))
|
accentColor: AppAccent.primary
|
||||||
|
)
|
||||||
|
.accessibilityHint(String(localized: "Enables or disables the ring light overlay"))
|
||||||
|
|
||||||
// Ring Size Slider
|
// Ring Size Slider
|
||||||
ringSizeSlider
|
ringSizeSlider
|
||||||
|
|
||||||
// Color Preset
|
// Color Preset
|
||||||
colorPresetSection
|
colorPresetSection
|
||||||
|
|
||||||
// Ring Light Brightness
|
// Ring Light Brightness
|
||||||
ringLightBrightnessSlider
|
ringLightBrightnessSlider
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Camera Controls Section
|
// MARK: - Camera Controls Section
|
||||||
|
|
||||||
SettingsSectionHeader(title: "Camera Controls", systemImage: "camera")
|
SettingsSectionHeader(title: "Camera Controls", systemImage: "camera", accentColor: AppAccent.primary)
|
||||||
|
|
||||||
// Camera Position
|
SettingsCard {
|
||||||
cameraPositionPicker
|
// Camera Position
|
||||||
|
cameraPositionPicker
|
||||||
|
|
||||||
// Flash Mode
|
// Flash Mode
|
||||||
flashModePicker
|
flashModePicker
|
||||||
|
|
||||||
// Flash Sync (premium)
|
// Flash Sync (premium)
|
||||||
premiumToggle(
|
premiumToggle(
|
||||||
title: String(localized: "Flash Sync"),
|
title: String(localized: "Flash Sync"),
|
||||||
subtitle: String(localized: "Use ring light color for screen 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")
|
||||||
)
|
)
|
||||||
|
|
||||||
// HDR Mode
|
// HDR Mode
|
||||||
hdrModePicker
|
hdrModePicker
|
||||||
|
|
||||||
// Center Stage (premium feature)
|
// Center Stage (premium feature)
|
||||||
centerStageToggle
|
centerStageToggle
|
||||||
|
|
||||||
// Photo Quality
|
// Photo Quality
|
||||||
photoQualityPicker
|
photoQualityPicker
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Display Section
|
// MARK: - Display Section
|
||||||
|
|
||||||
SettingsSectionHeader(title: "Display", systemImage: "eye")
|
SettingsSectionHeader(title: "Display", systemImage: "eye", accentColor: AppAccent.primary)
|
||||||
|
|
||||||
// True Mirror (premium)
|
SettingsCard {
|
||||||
premiumToggle(
|
// True Mirror (premium)
|
||||||
title: String(localized: "True Mirror"),
|
premiumToggle(
|
||||||
subtitle: String(localized: "Shows horizontally flipped preview like a real mirror"),
|
title: String(localized: "True Mirror"),
|
||||||
isOn: $viewModel.isMirrorFlipped,
|
subtitle: String(localized: "Shows horizontally flipped preview like a real mirror"),
|
||||||
accessibilityHint: String(localized: "Flips the camera preview horizontally")
|
isOn: $viewModel.isMirrorFlipped,
|
||||||
)
|
accessibilityHint: String(localized: "Flips the camera preview horizontally")
|
||||||
|
)
|
||||||
|
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: String(localized: "Grid Overlay"),
|
title: String(localized: "Grid Overlay"),
|
||||||
subtitle: String(localized: "Shows rule of thirds grid for composition"),
|
subtitle: String(localized: "Shows rule of thirds grid for composition"),
|
||||||
isOn: $viewModel.isGridVisible
|
isOn: $viewModel.isGridVisible,
|
||||||
)
|
accentColor: AppAccent.primary
|
||||||
.accessibilityHint(String(localized: "Shows a grid overlay to help compose your shot"))
|
)
|
||||||
|
.accessibilityHint(String(localized: "Shows a grid overlay to help compose your shot"))
|
||||||
|
|
||||||
// Skin Smoothing (premium)
|
// Skin Smoothing (premium)
|
||||||
premiumToggle(
|
premiumToggle(
|
||||||
title: String(localized: "Skin Smoothing"),
|
title: String(localized: "Skin Smoothing"),
|
||||||
subtitle: String(localized: "Applies subtle real-time skin smoothing"),
|
subtitle: String(localized: "Applies subtle real-time skin smoothing"),
|
||||||
isOn: $viewModel.isSkinSmoothingEnabled,
|
isOn: $viewModel.isSkinSmoothingEnabled,
|
||||||
accessibilityHint: String(localized: "Applies light skin smoothing to the camera preview")
|
accessibilityHint: String(localized: "Applies light skin smoothing to the camera preview")
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Capture Section
|
// MARK: - Capture Section
|
||||||
|
|
||||||
SettingsSectionHeader(title: "Capture", systemImage: "photo.on.rectangle")
|
SettingsSectionHeader(title: "Capture", systemImage: "photo.on.rectangle", accentColor: AppAccent.primary)
|
||||||
|
|
||||||
// Timer Selection
|
SettingsCard {
|
||||||
timerPicker
|
// Timer Selection
|
||||||
|
timerPicker
|
||||||
|
|
||||||
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,
|
||||||
)
|
accentColor: AppAccent.primary
|
||||||
.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", accentColor: AppStatus.warning)
|
||||||
|
|
||||||
proSection
|
proSection
|
||||||
|
|
||||||
// MARK: - Sync Section
|
// MARK: - Sync Section
|
||||||
|
|
||||||
SettingsSectionHeader(title: String(localized: "iCloud Sync"), systemImage: "icloud")
|
SettingsSectionHeader(title: String(localized: "iCloud Sync"), systemImage: "icloud", accentColor: AppAccent.primary)
|
||||||
|
|
||||||
iCloudSyncSection
|
SettingsCard {
|
||||||
|
iCloudSyncSection
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - About Section
|
// MARK: - About Section
|
||||||
|
|
||||||
SettingsSectionHeader(title: "About", systemImage: "info.circle")
|
SettingsSectionHeader(title: "About", systemImage: "info.circle", accentColor: AppAccent.primary)
|
||||||
|
|
||||||
acknowledgmentsSection
|
acknowledgmentsSection
|
||||||
|
|
||||||
// MARK: - Debug Section
|
// MARK: - Debug Section
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
SettingsSectionHeader(title: "Debug", systemImage: "ant.fill")
|
SettingsSectionHeader(title: "Debug", systemImage: "ant.fill", accentColor: AppStatus.error)
|
||||||
|
|
||||||
brandingDebugSection
|
SettingsCard {
|
||||||
|
brandingDebugSection
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
Spacer(minLength: Design.Spacing.xxxLarge)
|
Spacer(minLength: Design.Spacing.xxxLarge)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, Design.Spacing.large)
|
.padding(.horizontal, Design.Spacing.large)
|
||||||
}
|
}
|
||||||
.background(Color.Surface.overlay)
|
.background(AppSurface.primary)
|
||||||
.navigationTitle(String(localized: "Settings"))
|
.navigationTitle(String(localized: "Settings"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
@ -153,7 +168,7 @@ struct SettingsView: View {
|
|||||||
Button(String(localized: "Done")) {
|
Button(String(localized: "Done")) {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
.foregroundStyle(Color.Accent.primary)
|
.foregroundStyle(AppAccent.primary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -186,7 +201,7 @@ struct SettingsView: View {
|
|||||||
in: SettingsViewModel.minRingSize...SettingsViewModel.maxRingSize,
|
in: SettingsViewModel.minRingSize...SettingsViewModel.maxRingSize,
|
||||||
step: 5
|
step: 5
|
||||||
)
|
)
|
||||||
.tint(Color.Accent.primary)
|
.tint(AppAccent.primary)
|
||||||
|
|
||||||
// Large ring icon
|
// Large ring icon
|
||||||
Image(systemName: "circle")
|
Image(systemName: "circle")
|
||||||
@ -286,7 +301,7 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
Image(systemName: "crown.fill")
|
Image(systemName: "crown.fill")
|
||||||
.font(.system(size: Design.BaseFontSize.small))
|
.font(.system(size: Design.BaseFontSize.small))
|
||||||
.foregroundStyle(Color.Status.warning)
|
.foregroundStyle(AppStatus.warning)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(String(localized: "High Dynamic Range for better lighting in photos"))
|
Text(String(localized: "High Dynamic Range for better lighting in photos"))
|
||||||
@ -319,7 +334,7 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
Image(systemName: "crown.fill")
|
Image(systemName: "crown.fill")
|
||||||
.font(.system(size: Design.BaseFontSize.small))
|
.font(.system(size: Design.BaseFontSize.small))
|
||||||
.foregroundStyle(Color.Status.warning)
|
.foregroundStyle(AppStatus.warning)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(isPremiumUnlocked
|
Text(isPremiumUnlocked
|
||||||
@ -354,7 +369,7 @@ struct SettingsView: View {
|
|||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.background(
|
.background(
|
||||||
Capsule()
|
Capsule()
|
||||||
.fill(viewModel.photoQuality == quality ? Color.Accent.primary : Color.white.opacity(Design.Opacity.subtle))
|
.fill(viewModel.photoQuality == quality ? AppAccent.primary : Color.white.opacity(Design.Opacity.subtle))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
@ -415,7 +430,7 @@ struct SettingsView: View {
|
|||||||
in: 0.1...1.0,
|
in: 0.1...1.0,
|
||||||
step: 0.05
|
step: 0.05
|
||||||
)
|
)
|
||||||
.tint(Color.Accent.primary)
|
.tint(AppAccent.primary)
|
||||||
|
|
||||||
Image(systemName: "sun.max.fill")
|
Image(systemName: "sun.max.fill")
|
||||||
.font(.system(size: Design.BaseFontSize.large))
|
.font(.system(size: Design.BaseFontSize.large))
|
||||||
@ -443,7 +458,7 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
Image(systemName: "crown.fill")
|
Image(systemName: "crown.fill")
|
||||||
.font(.system(size: Design.BaseFontSize.small))
|
.font(.system(size: Design.BaseFontSize.small))
|
||||||
.foregroundStyle(Color.Status.warning)
|
.foregroundStyle(AppStatus.warning)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(String(localized: "Automatically keeps you centered in the frame"))
|
Text(String(localized: "Automatically keeps you centered in the frame"))
|
||||||
@ -451,7 +466,7 @@ struct SettingsView: View {
|
|||||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(Color.Accent.primary)
|
.tint(AppAccent.primary)
|
||||||
.padding(.vertical, Design.Spacing.xSmall)
|
.padding(.vertical, Design.Spacing.xSmall)
|
||||||
.disabled(!isPremiumUnlocked)
|
.disabled(!isPremiumUnlocked)
|
||||||
.accessibilityLabel(String(localized: "Enable Center Stage"))
|
.accessibilityLabel(String(localized: "Enable Center Stage"))
|
||||||
@ -477,7 +492,7 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
Image(systemName: "crown.fill")
|
Image(systemName: "crown.fill")
|
||||||
.font(.system(size: Design.BaseFontSize.small))
|
.font(.system(size: Design.BaseFontSize.small))
|
||||||
.foregroundStyle(Color.Status.warning)
|
.foregroundStyle(AppStatus.warning)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(isPremiumUnlocked
|
Text(isPremiumUnlocked
|
||||||
@ -512,7 +527,7 @@ struct SettingsView: View {
|
|||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.background(
|
.background(
|
||||||
Capsule()
|
Capsule()
|
||||||
.fill(viewModel.selectedTimer == option ? Color.Accent.primary : Color.white.opacity(Design.Opacity.subtle))
|
.fill(viewModel.selectedTimer == option ? AppAccent.primary : Color.white.opacity(Design.Opacity.subtle))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
@ -536,7 +551,7 @@ struct SettingsView: View {
|
|||||||
HStack(spacing: Design.Spacing.medium) {
|
HStack(spacing: Design.Spacing.medium) {
|
||||||
Image(systemName: "crown.fill")
|
Image(systemName: "crown.fill")
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.foregroundStyle(Color.Status.warning)
|
.foregroundStyle(AppStatus.warning)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
||||||
Text(String(localized: "Upgrade to Pro"))
|
Text(String(localized: "Upgrade to Pro"))
|
||||||
@ -557,8 +572,8 @@ struct SettingsView: View {
|
|||||||
.padding(Design.Spacing.medium)
|
.padding(Design.Spacing.medium)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
|
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
|
||||||
.fill(Color.Accent.primary.opacity(Design.Opacity.subtle))
|
.fill(AppAccent.primary.opacity(Design.Opacity.subtle))
|
||||||
.strokeBorder(Color.Accent.primary.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.thin)
|
.strokeBorder(AppAccent.primary.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.thin)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
@ -584,7 +599,7 @@ struct SettingsView: View {
|
|||||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(Color.Accent.primary)
|
.tint(AppAccent.primary)
|
||||||
.padding(.vertical, Design.Spacing.xSmall)
|
.padding(.vertical, Design.Spacing.xSmall)
|
||||||
.disabled(!viewModel.iCloudAvailable)
|
.disabled(!viewModel.iCloudAvailable)
|
||||||
.accessibilityHint(String(localized: "Syncs settings across all your devices via iCloud"))
|
.accessibilityHint(String(localized: "Syncs settings across all your devices via iCloud"))
|
||||||
@ -607,7 +622,7 @@ struct SettingsView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
Text(String(localized: "Sync Now"))
|
Text(String(localized: "Sync Now"))
|
||||||
.font(.system(size: Design.BaseFontSize.caption, weight: .medium))
|
.font(.system(size: Design.BaseFontSize.caption, weight: .medium))
|
||||||
.foregroundStyle(Color.Accent.primary)
|
.foregroundStyle(AppAccent.primary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.top, Design.Spacing.xSmall)
|
.padding(.top, Design.Spacing.xSmall)
|
||||||
@ -626,9 +641,9 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
private var syncStatusColor: Color {
|
private var syncStatusColor: Color {
|
||||||
if !viewModel.hasCompletedInitialSync {
|
if !viewModel.hasCompletedInitialSync {
|
||||||
return Color.Status.warning
|
return AppStatus.warning
|
||||||
}
|
}
|
||||||
return Color.Status.success
|
return AppStatus.success
|
||||||
}
|
}
|
||||||
|
|
||||||
private var syncStatusText: String {
|
private var syncStatusText: String {
|
||||||
@ -672,7 +687,7 @@ struct SettingsView: View {
|
|||||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
}
|
}
|
||||||
.padding(Design.Spacing.medium)
|
.padding(Design.Spacing.medium)
|
||||||
.background(Color.Surface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
|
.background(AppSurface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
@ -703,7 +718,7 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
Image(systemName: "crown.fill")
|
Image(systemName: "crown.fill")
|
||||||
.font(.system(size: Design.BaseFontSize.small))
|
.font(.system(size: Design.BaseFontSize.small))
|
||||||
.foregroundStyle(Color.Status.warning)
|
.foregroundStyle(AppStatus.warning)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(subtitle)
|
Text(subtitle)
|
||||||
@ -711,7 +726,7 @@ struct SettingsView: View {
|
|||||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(Color.Accent.primary)
|
.tint(AppAccent.primary)
|
||||||
.padding(.vertical, Design.Spacing.xSmall)
|
.padding(.vertical, Design.Spacing.xSmall)
|
||||||
.disabled(!isPremiumUnlocked)
|
.disabled(!isPremiumUnlocked)
|
||||||
.accessibilityHint(accessibilityHint)
|
.accessibilityHint(accessibilityHint)
|
||||||
@ -744,7 +759,7 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
Image(systemName: "crown.fill")
|
Image(systemName: "crown.fill")
|
||||||
.font(.system(size: Design.BaseFontSize.small))
|
.font(.system(size: Design.BaseFontSize.small))
|
||||||
.foregroundStyle(Color.Status.warning)
|
.foregroundStyle(AppStatus.warning)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
@ -764,7 +779,7 @@ struct SettingsView: View {
|
|||||||
subtitle: "Unlock all premium features for testing",
|
subtitle: "Unlock all premium features for testing",
|
||||||
isOn: $viewModel.isDebugPremiumEnabled
|
isOn: $viewModel.isDebugPremiumEnabled
|
||||||
)
|
)
|
||||||
.tint(Color.Status.warning)
|
.tint(AppStatus.warning)
|
||||||
// Icon Generator
|
// Icon Generator
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
IconGeneratorView(config: .selfieCam, appName: "SelfieCam")
|
IconGeneratorView(config: .selfieCam, appName: "SelfieCam")
|
||||||
@ -787,7 +802,7 @@ struct SettingsView: View {
|
|||||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
}
|
}
|
||||||
.padding(Design.Spacing.medium)
|
.padding(Design.Spacing.medium)
|
||||||
.background(Color.Surface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
|
.background(AppSurface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
@ -817,7 +832,7 @@ struct SettingsView: View {
|
|||||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
}
|
}
|
||||||
.padding(Design.Spacing.medium)
|
.padding(Design.Spacing.medium)
|
||||||
.background(Color.Surface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
|
.background(AppSurface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
@ -827,6 +842,29 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Settings Card Container
|
||||||
|
|
||||||
|
/// A card container that provides visual grouping for settings sections.
|
||||||
|
/// Uses the app's branded surface colors for separation from the background.
|
||||||
|
private struct SettingsCard<Content: View>: View {
|
||||||
|
@ViewBuilder let content: Content
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
.padding(Design.Spacing.medium)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
||||||
|
.fill(AppSurface.card)
|
||||||
|
)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
||||||
|
.strokeBorder(AppBorder.subtle, lineWidth: Design.LineWidth.thin)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
SettingsView(viewModel: SettingsViewModel(), showPaywall: .constant(false))
|
SettingsView(viewModel: SettingsViewModel(), showPaywall: .constant(false))
|
||||||
.preferredColorScheme(.dark)
|
.preferredColorScheme(.dark)
|
||||||
|
|||||||
144
SelfieCam/Shared/SelfieCamTheme.swift
Normal file
144
SelfieCam/Shared/SelfieCamTheme.swift
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
//
|
||||||
|
// SelfieCamTheme.swift
|
||||||
|
// SelfieCam
|
||||||
|
//
|
||||||
|
// Custom color theme for SelfieCam matching the app's branding.
|
||||||
|
// Uses magenta/rose tinted surfaces with branded accent colors.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Bedrock
|
||||||
|
|
||||||
|
// MARK: - SelfieCam Surface Colors
|
||||||
|
|
||||||
|
/// Surface colors with a subtle rose/magenta tint to match app branding.
|
||||||
|
/// Provides more visual separation than neutral grays.
|
||||||
|
public enum SelfieCamSurfaceColors: SurfaceColorProvider {
|
||||||
|
/// Primary background - deep rose-tinted dark
|
||||||
|
public static let primary = Color(red: 0.08, green: 0.06, blue: 0.10)
|
||||||
|
|
||||||
|
/// Secondary/elevated surface - slightly lighter with rose tint
|
||||||
|
public static let secondary = Color(red: 0.12, green: 0.08, blue: 0.14)
|
||||||
|
|
||||||
|
/// Tertiary/card surface - more elevated
|
||||||
|
public static let tertiary = Color(red: 0.16, green: 0.11, blue: 0.18)
|
||||||
|
|
||||||
|
/// Overlay background (for sheets/modals) - deep with subtle rose
|
||||||
|
public static let overlay = Color(red: 0.10, green: 0.07, blue: 0.12)
|
||||||
|
|
||||||
|
/// Card/grouped element background - distinct from primary
|
||||||
|
public static let card = Color(red: 0.14, green: 0.10, blue: 0.16)
|
||||||
|
|
||||||
|
/// Subtle fill for grouped content sections
|
||||||
|
public static let groupedFill = Color(red: 0.12, green: 0.09, blue: 0.14)
|
||||||
|
|
||||||
|
/// Section fill for list sections - slightly more visible
|
||||||
|
public static let sectionFill = Color(red: 0.16, green: 0.12, blue: 0.18)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - SelfieCam Text Colors
|
||||||
|
|
||||||
|
/// Text colors optimized for rose-tinted dark backgrounds.
|
||||||
|
public enum SelfieCamTextColors: TextColorProvider {
|
||||||
|
public static let primary = Color.white
|
||||||
|
public static let secondary = Color.white.opacity(Design.Opacity.accent)
|
||||||
|
public static let tertiary = Color.white.opacity(Design.Opacity.medium)
|
||||||
|
public static let disabled = Color.white.opacity(Design.Opacity.light)
|
||||||
|
public static let placeholder = Color.white.opacity(Design.Opacity.overlay)
|
||||||
|
public static let inverse = Color.black
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - SelfieCam Accent Colors
|
||||||
|
|
||||||
|
/// Accent colors derived from the app's branding magenta/rose.
|
||||||
|
public enum SelfieCamAccentColors: AccentColorProvider {
|
||||||
|
/// Primary accent - bright magenta/rose from branding
|
||||||
|
public static let primary = Color(red: 0.85, green: 0.25, blue: 0.45)
|
||||||
|
|
||||||
|
/// Light variant - softer pink
|
||||||
|
public static let light = Color(red: 0.95, green: 0.45, blue: 0.60)
|
||||||
|
|
||||||
|
/// Dark variant - deeper magenta
|
||||||
|
public static let dark = Color(red: 0.65, green: 0.18, blue: 0.35)
|
||||||
|
|
||||||
|
/// Secondary accent - soft cream/warm white for contrast
|
||||||
|
public static let secondary = Color(red: 1.0, green: 0.95, blue: 0.90)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - SelfieCam Button Colors
|
||||||
|
|
||||||
|
/// Button colors matching the branded theme.
|
||||||
|
public enum SelfieCamButtonColors: ButtonColorProvider {
|
||||||
|
public static let primaryLight = Color(red: 0.95, green: 0.40, blue: 0.55)
|
||||||
|
public static let primaryDark = Color(red: 0.75, green: 0.20, blue: 0.40)
|
||||||
|
public static let secondary = Color.white.opacity(Design.Opacity.subtle)
|
||||||
|
public static let destructive = Color.red.opacity(Design.Opacity.heavy)
|
||||||
|
public static let cancelText = Color.white.opacity(Design.Opacity.strong)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - SelfieCam Status Colors
|
||||||
|
|
||||||
|
/// Standard semantic status colors.
|
||||||
|
public enum SelfieCamStatusColors: StatusColorProvider {
|
||||||
|
public static let success = Color(red: 0.2, green: 0.8, blue: 0.4)
|
||||||
|
public static let warning = Color(red: 1.0, green: 0.75, blue: 0.2)
|
||||||
|
public static let error = Color(red: 0.9, green: 0.3, blue: 0.3)
|
||||||
|
public static let info = Color(red: 0.5, green: 0.7, blue: 0.95)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - SelfieCam Border Colors
|
||||||
|
|
||||||
|
/// Border colors for the rose-tinted theme.
|
||||||
|
public enum SelfieCamBorderColors: BorderColorProvider {
|
||||||
|
public static let subtle = Color.white.opacity(Design.Opacity.subtle)
|
||||||
|
public static let standard = Color.white.opacity(Design.Opacity.hint)
|
||||||
|
public static let emphasized = Color.white.opacity(Design.Opacity.light)
|
||||||
|
public static let selected = SelfieCamAccentColors.primary.opacity(Design.Opacity.medium)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - SelfieCam Interactive Colors
|
||||||
|
|
||||||
|
/// Interactive state colors for the theme.
|
||||||
|
public enum SelfieCamInteractiveColors: InteractiveColorProvider {
|
||||||
|
public static let selected = SelfieCamAccentColors.primary.opacity(Design.Opacity.selection)
|
||||||
|
public static let hover = Color.white.opacity(Design.Opacity.subtle)
|
||||||
|
public static let pressed = Color.white.opacity(Design.Opacity.hint)
|
||||||
|
public static let focus = SelfieCamAccentColors.light
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - SelfieCam Theme
|
||||||
|
|
||||||
|
/// The complete SelfieCam color theme.
|
||||||
|
///
|
||||||
|
/// Use this theme for consistent branded colors throughout the app:
|
||||||
|
/// ```swift
|
||||||
|
/// .background(SelfieCamTheme.Surface.primary)
|
||||||
|
/// .foregroundStyle(SelfieCamTheme.Accent.primary)
|
||||||
|
/// ```
|
||||||
|
public enum SelfieCamTheme: AppColorTheme {
|
||||||
|
public typealias Surface = SelfieCamSurfaceColors
|
||||||
|
public typealias Text = SelfieCamTextColors
|
||||||
|
public typealias Accent = SelfieCamAccentColors
|
||||||
|
public typealias Button = SelfieCamButtonColors
|
||||||
|
public typealias Status = SelfieCamStatusColors
|
||||||
|
public typealias Border = SelfieCamBorderColors
|
||||||
|
public typealias Interactive = SelfieCamInteractiveColors
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Convenience Typealiases
|
||||||
|
|
||||||
|
/// Short typealiases for cleaner usage throughout the app.
|
||||||
|
/// These avoid conflicts with Bedrock's default typealiases by using unique names.
|
||||||
|
///
|
||||||
|
/// Usage:
|
||||||
|
/// ```swift
|
||||||
|
/// .background(AppSurface.primary)
|
||||||
|
/// .foregroundStyle(AppAccent.primary)
|
||||||
|
/// ```
|
||||||
|
typealias AppSurface = SelfieCamSurfaceColors
|
||||||
|
typealias AppTextColors = SelfieCamTextColors
|
||||||
|
typealias AppAccent = SelfieCamAccentColors
|
||||||
|
typealias AppButtonColors = SelfieCamButtonColors
|
||||||
|
typealias AppStatus = SelfieCamStatusColors
|
||||||
|
typealias AppBorder = SelfieCamBorderColors
|
||||||
|
typealias AppInteractive = SelfieCamInteractiveColors
|
||||||
Loading…
Reference in New Issue
Block a user