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
|
||||
|
||||
Potential areas for expansion:
|
||||
@ -312,6 +367,7 @@ Potential areas for expansion:
|
||||
- [ ] Apple Watch remote trigger
|
||||
- [ ] Export presets (aspect ratios, watermarks)
|
||||
- [ ] Social sharing integrations
|
||||
- [ ] Camera Control button swipe-to-zoom (if Apple makes API public)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
// CaptureEventInteraction.swift
|
||||
// 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.
|
||||
//
|
||||
|
||||
@ -18,18 +18,20 @@ import UIKit
|
||||
/// - **Camera Control button** (iPhone 16+): Full press to capture, light press for focus/exposure
|
||||
/// - **Action button** (iPhone 15 Pro+): When configured for camera
|
||||
/// - **Volume buttons**: Hardware shutter via system integration
|
||||
///
|
||||
/// - Note: Requires iOS 17.2+ for `AVCaptureEventInteraction` API
|
||||
@Observable @MainActor
|
||||
final class CaptureEventManager {
|
||||
private(set) var isActive = false
|
||||
private(set) var isFocusLocked = false
|
||||
|
||||
// Store as Any to avoid availability issues with the type declaration
|
||||
private var eventInteraction: AnyObject?
|
||||
private var eventInteraction: AVCaptureEventInteraction?
|
||||
private var onCapture: (() -> 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
|
||||
/// - Parameters:
|
||||
/// - onCapture: Called when a capture event is triggered (full press)
|
||||
@ -42,19 +44,10 @@ final class CaptureEventManager {
|
||||
self.onFocusLock = onFocusLock
|
||||
}
|
||||
|
||||
/// Creates and returns the UIInteraction to be added to a view
|
||||
/// - Returns: The configured interaction, or nil if not available
|
||||
func createInteraction() -> (any UIInteraction)? {
|
||||
guard #available(iOS 17.2, *) else {
|
||||
Design.debugLog("CaptureEventInteraction: iOS 17.2+ required")
|
||||
return nil
|
||||
}
|
||||
/// Creates and returns the AVCaptureEventInteraction to be added to a view
|
||||
func createInteraction() -> AVCaptureEventInteraction {
|
||||
Design.debugLog("📸 [CameraControl] Creating AVCaptureEventInteraction...")
|
||||
|
||||
return createCaptureEventInteraction()
|
||||
}
|
||||
|
||||
@available(iOS 17.2, *)
|
||||
private func createCaptureEventInteraction() -> AVCaptureEventInteraction {
|
||||
let interaction = AVCaptureEventInteraction(
|
||||
primary: { [weak self] event 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.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
|
||||
}
|
||||
@ -80,184 +80,110 @@ final class CaptureEventManager {
|
||||
eventInteraction = nil
|
||||
isActive = false
|
||||
isFocusLocked = false
|
||||
Design.debugLog("CaptureEventInteraction: Hardware capture events disabled")
|
||||
Design.debugLog("📸 [CameraControl] Hardware capture events disabled")
|
||||
}
|
||||
|
||||
// MARK: - Event Handlers
|
||||
|
||||
@available(iOS 17.2, *)
|
||||
private func handlePrimaryEvent(_ event: AVCaptureEvent) {
|
||||
Design.debugLog("📸 [CameraControl] PRIMARY - phase: \(event.phase.rawValue)")
|
||||
|
||||
switch event.phase {
|
||||
case .began:
|
||||
Design.debugLog("CaptureEventInteraction: Primary event began (press detected)")
|
||||
// Haptic feedback on press
|
||||
triggerHaptic(.light)
|
||||
Design.debugLog("📸 [CameraControl] PRIMARY BEGAN - Full press started")
|
||||
triggerHaptic(.medium)
|
||||
|
||||
case .ended:
|
||||
Design.debugLog("CaptureEventInteraction: Primary event ended (capture)")
|
||||
Design.debugLog("📸 [CameraControl] PRIMARY ENDED - Triggering capture!")
|
||||
triggerHaptic(.heavy)
|
||||
onCapture?()
|
||||
|
||||
case .cancelled:
|
||||
Design.debugLog("CaptureEventInteraction: Primary event cancelled")
|
||||
Design.debugLog("📸 [CameraControl] PRIMARY CANCELLED")
|
||||
|
||||
@unknown default:
|
||||
break
|
||||
Design.debugLog("📸 [CameraControl] PRIMARY UNKNOWN phase: \(event.phase.rawValue)")
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 17.2, *)
|
||||
private func handleSecondaryEvent(_ event: AVCaptureEvent) {
|
||||
Design.debugLog("🔒 [CameraControl] SECONDARY - phase: \(event.phase.rawValue)")
|
||||
|
||||
switch event.phase {
|
||||
case .began:
|
||||
Design.debugLog("CaptureEventInteraction: Secondary event began (focus lock)")
|
||||
Design.debugLog("🔒 [CameraControl] SECONDARY BEGAN - Light press! Locking focus...")
|
||||
isFocusLocked = true
|
||||
onFocusLock?(true)
|
||||
triggerHaptic(.light)
|
||||
triggerHaptic(.medium)
|
||||
|
||||
case .ended:
|
||||
Design.debugLog("CaptureEventInteraction: Secondary event ended (focus unlock)")
|
||||
Design.debugLog("🔒 [CameraControl] SECONDARY ENDED - Unlocking focus")
|
||||
isFocusLocked = false
|
||||
onFocusLock?(false)
|
||||
triggerHaptic(.light)
|
||||
|
||||
case .cancelled:
|
||||
Design.debugLog("CaptureEventInteraction: Secondary event cancelled")
|
||||
Design.debugLog("🔒 [CameraControl] SECONDARY CANCELLED")
|
||||
isFocusLocked = false
|
||||
onFocusLock?(false)
|
||||
|
||||
@unknown default:
|
||||
break
|
||||
Design.debugLog("🔒 [CameraControl] SECONDARY UNKNOWN phase: \(event.phase.rawValue)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Haptic Feedback
|
||||
|
||||
/// Prepares haptic engines for low-latency feedback
|
||||
func prepareHaptics() {
|
||||
lightHaptic.prepare()
|
||||
mediumHaptic.prepare()
|
||||
heavyHaptic.prepare()
|
||||
}
|
||||
|
||||
private func triggerHaptic(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
|
||||
switch style {
|
||||
case .light:
|
||||
lightHaptic.impactOccurred()
|
||||
lightHaptic.prepare()
|
||||
case .medium:
|
||||
mediumHaptic.impactOccurred()
|
||||
mediumHaptic.prepare()
|
||||
case .heavy:
|
||||
heavyHaptic.impactOccurred()
|
||||
heavyHaptic.prepare()
|
||||
default:
|
||||
let generator = UIImpactFeedbackGenerator(style: style)
|
||||
generator.impactOccurred()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SwiftUI View for Hosting the Interaction
|
||||
|
||||
/// A SwiftUI view that hosts AVCaptureEventInteraction for Camera Control button support
|
||||
///
|
||||
/// Add this view to your camera hierarchy to enable hardware capture events.
|
||||
/// The view is invisible and passes through all touches.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```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
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Device Capability Check
|
||||
|
||||
extension CaptureEventManager {
|
||||
/// 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 {
|
||||
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
|
||||
}
|
||||
|
||||
/// Returns a description of supported hardware capture methods
|
||||
static var supportedHardwareMethods: String {
|
||||
var methods: [String] = []
|
||||
|
||||
if #available(iOS 17.2, *) {
|
||||
methods.append("Camera Control (iPhone 16+)")
|
||||
methods.append("Action Button")
|
||||
methods.append("Volume Buttons")
|
||||
} else {
|
||||
methods.append("Volume Buttons (legacy)")
|
||||
return "Camera Control (iPhone 16+), Action Button, Volume Buttons"
|
||||
}
|
||||
}
|
||||
|
||||
return methods.joined(separator: ", ")
|
||||
// MARK: - AVCaptureEventPhase Extension
|
||||
|
||||
extension AVCaptureEventPhase {
|
||||
/// Converts AVCaptureEventPhase to our CaptureEventPhase enum
|
||||
var toCaptureEventPhase: CaptureEventPhase {
|
||||
switch self {
|
||||
case .began: return .began
|
||||
case .ended: return .ended
|
||||
case .cancelled: return .cancelled
|
||||
@unknown default: return .cancelled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import AVKit
|
||||
import SwiftUI
|
||||
import Bedrock
|
||||
import MijickCamera
|
||||
@ -52,6 +53,18 @@ struct CustomCameraScreen: MCameraScreen {
|
||||
createCameraOutputView()
|
||||
.ignoresSafeArea()
|
||||
.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
|
||||
.gesture(
|
||||
MagnificationGesture()
|
||||
@ -167,16 +180,6 @@ struct CustomCameraScreen: MCameraScreen {
|
||||
// Hidden volume view to prevent system HUD when using volume buttons as shutter
|
||||
HiddenVolumeView()
|
||||
.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.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)
|
||||
|
||||
/// Handles focus/exposure lock from Camera Control button light press
|
||||
/// - Parameter locked: Whether focus/exposure is being locked
|
||||
private func handleFocusLock(_ locked: Bool) {
|
||||
Design.debugLog("🔒 [FocusLock] handleFocusLock called with locked=\(locked)")
|
||||
|
||||
guard let device = AVCaptureDevice.default(
|
||||
.builtInWideAngleCamera,
|
||||
for: .video,
|
||||
position: cameraPosition == .front ? .front : .back
|
||||
) else { return }
|
||||
) else {
|
||||
Design.debugLog("🔒 [FocusLock] ⚠️ No capture device found!")
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try device.lockForConfiguration()
|
||||
@ -440,25 +496,31 @@ struct CustomCameraScreen: MCameraScreen {
|
||||
// Lock current focus and exposure
|
||||
if device.isFocusModeSupported(.locked) {
|
||||
device.focusMode = .locked
|
||||
Design.debugLog("🔒 [FocusLock] ✅ Focus LOCKED")
|
||||
} else {
|
||||
Design.debugLog("🔒 [FocusLock] ⚠️ Focus lock not supported")
|
||||
}
|
||||
if device.isExposureModeSupported(.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 {
|
||||
// Restore continuous autofocus and auto exposure
|
||||
if device.isFocusModeSupported(.continuousAutoFocus) {
|
||||
device.focusMode = .continuousAutoFocus
|
||||
Design.debugLog("🔒 [FocusLock] ✅ Focus UNLOCKED (continuous)")
|
||||
}
|
||||
if device.isExposureModeSupported(.continuousAutoExposure) {
|
||||
device.exposureMode = .continuousAutoExposure
|
||||
Design.debugLog("🔒 [FocusLock] ✅ Exposure UNLOCKED (continuous)")
|
||||
}
|
||||
Design.debugLog("Focus/Exposure unlocked via Camera Control")
|
||||
}
|
||||
|
||||
device.unlockForConfiguration()
|
||||
} 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)
|
||||
}
|
||||
.background(Color.Surface.overlay)
|
||||
.background(AppSurface.overlay)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
@ -102,11 +102,11 @@ private struct ProductPackageButton: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.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))
|
||||
.overlay(
|
||||
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)"))
|
||||
@ -123,7 +123,7 @@ struct BenefitRow: View {
|
||||
HStack(spacing: Design.Spacing.medium) {
|
||||
Image(systemName: image)
|
||||
.font(.title2)
|
||||
.foregroundStyle(Color.Accent.primary)
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
.frame(width: Design.IconSize.xLarge)
|
||||
|
||||
Text(text)
|
||||
|
||||
@ -31,7 +31,7 @@ struct ColorPresetButton: View {
|
||||
.overlay(
|
||||
Circle()
|
||||
.strokeBorder(
|
||||
isSelected ? Color.Accent.primary : Color.Border.subtle,
|
||||
isSelected ? AppAccent.primary : AppBorder.subtle,
|
||||
lineWidth: isSelected ? Design.LineWidth.thick : Design.LineWidth.thin
|
||||
)
|
||||
)
|
||||
@ -61,13 +61,13 @@ struct ColorPresetButton: View {
|
||||
if preset.isPremium {
|
||||
Image(systemName: isPremiumUnlocked ? "crown.fill" : "crown")
|
||||
.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)
|
||||
.background(
|
||||
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)
|
||||
|
||||
@ -36,7 +36,7 @@ struct CustomColorPickerButton: View {
|
||||
.overlay(
|
||||
Circle()
|
||||
.strokeBorder(
|
||||
isSelected ? Color.Accent.primary : Color.Border.subtle,
|
||||
isSelected ? AppAccent.primary : AppBorder.subtle,
|
||||
lineWidth: isSelected ? Design.LineWidth.thick : Design.LineWidth.thin
|
||||
)
|
||||
)
|
||||
@ -53,12 +53,12 @@ struct CustomColorPickerButton: View {
|
||||
|
||||
Image(systemName: "crown.fill")
|
||||
.font(.system(size: Design.BaseFontSize.xxSmall))
|
||||
.foregroundStyle(Color.Status.warning)
|
||||
.foregroundStyle(AppStatus.warning)
|
||||
}
|
||||
.padding(Design.Spacing.xSmall)
|
||||
.background(
|
||||
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"))
|
||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
||||
@ -78,7 +78,7 @@ struct CustomColorPickerButton: View {
|
||||
.frame(width: Design.Size.avatarSmall, height: Design.Size.avatarSmall)
|
||||
.overlay(
|
||||
Circle()
|
||||
.strokeBorder(Color.Border.subtle, lineWidth: Design.LineWidth.thin)
|
||||
.strokeBorder(AppBorder.subtle, lineWidth: Design.LineWidth.thin)
|
||||
)
|
||||
|
||||
// Lock overlay
|
||||
@ -99,7 +99,7 @@ struct CustomColorPickerButton: View {
|
||||
|
||||
Image(systemName: "crown")
|
||||
.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)
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ struct LicensesView: View {
|
||||
}
|
||||
.padding(Design.Spacing.large)
|
||||
}
|
||||
.background(Color.Surface.overlay)
|
||||
.background(AppSurface.overlay)
|
||||
.navigationTitle(String(localized: "Open Source Licenses"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
@ -50,7 +50,7 @@ struct LicensesView: View {
|
||||
HStack {
|
||||
Label(license, systemImage: "doc.text")
|
||||
.font(.system(size: Design.BaseFontSize.xSmall))
|
||||
.foregroundStyle(Color.Accent.primary)
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
|
||||
Spacer()
|
||||
|
||||
@ -58,13 +58,13 @@ struct LicensesView: View {
|
||||
Link(destination: linkURL) {
|
||||
Label(String(localized: "View on GitHub"), systemImage: "arrow.up.right.square")
|
||||
.font(.system(size: Design.BaseFontSize.xSmall))
|
||||
.foregroundStyle(Color.Accent.primary)
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, Design.Spacing.xSmall)
|
||||
}
|
||||
.padding(Design.Spacing.medium)
|
||||
.background(Color.Surface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
|
||||
.background(AppSurface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,13 +28,15 @@ struct SettingsView: View {
|
||||
|
||||
// MARK: - Ring Light Section
|
||||
|
||||
SettingsSectionHeader(title: "Ring Light", systemImage: "light.max")
|
||||
SettingsSectionHeader(title: "Ring Light", systemImage: "light.max", accentColor: AppAccent.primary)
|
||||
|
||||
SettingsCard {
|
||||
// Ring Light Enabled
|
||||
SettingsToggle(
|
||||
title: String(localized: "Enable Ring Light"),
|
||||
subtitle: String(localized: "Show colored light ring around camera preview"),
|
||||
isOn: $viewModel.isRingLightEnabled
|
||||
isOn: $viewModel.isRingLightEnabled,
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
.accessibilityHint(String(localized: "Enables or disables the ring light overlay"))
|
||||
|
||||
@ -46,11 +48,13 @@ struct SettingsView: View {
|
||||
|
||||
// Ring Light Brightness
|
||||
ringLightBrightnessSlider
|
||||
}
|
||||
|
||||
// MARK: - Camera Controls Section
|
||||
|
||||
SettingsSectionHeader(title: "Camera Controls", systemImage: "camera")
|
||||
SettingsSectionHeader(title: "Camera Controls", systemImage: "camera", accentColor: AppAccent.primary)
|
||||
|
||||
SettingsCard {
|
||||
// Camera Position
|
||||
cameraPositionPicker
|
||||
|
||||
@ -73,11 +77,13 @@ struct SettingsView: View {
|
||||
|
||||
// Photo Quality
|
||||
photoQualityPicker
|
||||
}
|
||||
|
||||
// MARK: - Display Section
|
||||
|
||||
SettingsSectionHeader(title: "Display", systemImage: "eye")
|
||||
SettingsSectionHeader(title: "Display", systemImage: "eye", accentColor: AppAccent.primary)
|
||||
|
||||
SettingsCard {
|
||||
// True Mirror (premium)
|
||||
premiumToggle(
|
||||
title: String(localized: "True Mirror"),
|
||||
@ -89,7 +95,8 @@ struct SettingsView: View {
|
||||
SettingsToggle(
|
||||
title: String(localized: "Grid Overlay"),
|
||||
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"))
|
||||
|
||||
@ -100,52 +107,60 @@ struct SettingsView: View {
|
||||
isOn: $viewModel.isSkinSmoothingEnabled,
|
||||
accessibilityHint: String(localized: "Applies light skin smoothing to the camera preview")
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Capture Section
|
||||
|
||||
SettingsSectionHeader(title: "Capture", systemImage: "photo.on.rectangle")
|
||||
SettingsSectionHeader(title: "Capture", systemImage: "photo.on.rectangle", accentColor: AppAccent.primary)
|
||||
|
||||
SettingsCard {
|
||||
// Timer Selection
|
||||
timerPicker
|
||||
|
||||
SettingsToggle(
|
||||
title: String(localized: "Auto-Save"),
|
||||
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"))
|
||||
}
|
||||
|
||||
// MARK: - Pro Section
|
||||
|
||||
SettingsSectionHeader(title: "Pro", systemImage: "crown")
|
||||
SettingsSectionHeader(title: "Pro", systemImage: "crown", accentColor: AppStatus.warning)
|
||||
|
||||
proSection
|
||||
|
||||
// MARK: - Sync Section
|
||||
|
||||
SettingsSectionHeader(title: String(localized: "iCloud Sync"), systemImage: "icloud")
|
||||
SettingsSectionHeader(title: String(localized: "iCloud Sync"), systemImage: "icloud", accentColor: AppAccent.primary)
|
||||
|
||||
SettingsCard {
|
||||
iCloudSyncSection
|
||||
}
|
||||
|
||||
// MARK: - About Section
|
||||
|
||||
SettingsSectionHeader(title: "About", systemImage: "info.circle")
|
||||
SettingsSectionHeader(title: "About", systemImage: "info.circle", accentColor: AppAccent.primary)
|
||||
|
||||
acknowledgmentsSection
|
||||
|
||||
// MARK: - Debug Section
|
||||
|
||||
#if DEBUG
|
||||
SettingsSectionHeader(title: "Debug", systemImage: "ant.fill")
|
||||
SettingsSectionHeader(title: "Debug", systemImage: "ant.fill", accentColor: AppStatus.error)
|
||||
|
||||
SettingsCard {
|
||||
brandingDebugSection
|
||||
}
|
||||
#endif
|
||||
|
||||
Spacer(minLength: Design.Spacing.xxxLarge)
|
||||
}
|
||||
.padding(.horizontal, Design.Spacing.large)
|
||||
}
|
||||
.background(Color.Surface.overlay)
|
||||
.background(AppSurface.primary)
|
||||
.navigationTitle(String(localized: "Settings"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
@ -153,7 +168,7 @@ struct SettingsView: View {
|
||||
Button(String(localized: "Done")) {
|
||||
dismiss()
|
||||
}
|
||||
.foregroundStyle(Color.Accent.primary)
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -186,7 +201,7 @@ struct SettingsView: View {
|
||||
in: SettingsViewModel.minRingSize...SettingsViewModel.maxRingSize,
|
||||
step: 5
|
||||
)
|
||||
.tint(Color.Accent.primary)
|
||||
.tint(AppAccent.primary)
|
||||
|
||||
// Large ring icon
|
||||
Image(systemName: "circle")
|
||||
@ -286,7 +301,7 @@ struct SettingsView: View {
|
||||
|
||||
Image(systemName: "crown.fill")
|
||||
.font(.system(size: Design.BaseFontSize.small))
|
||||
.foregroundStyle(Color.Status.warning)
|
||||
.foregroundStyle(AppStatus.warning)
|
||||
}
|
||||
|
||||
Text(String(localized: "High Dynamic Range for better lighting in photos"))
|
||||
@ -319,7 +334,7 @@ struct SettingsView: View {
|
||||
|
||||
Image(systemName: "crown.fill")
|
||||
.font(.system(size: Design.BaseFontSize.small))
|
||||
.foregroundStyle(Color.Status.warning)
|
||||
.foregroundStyle(AppStatus.warning)
|
||||
}
|
||||
|
||||
Text(isPremiumUnlocked
|
||||
@ -354,7 +369,7 @@ struct SettingsView: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(
|
||||
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)
|
||||
@ -415,7 +430,7 @@ struct SettingsView: View {
|
||||
in: 0.1...1.0,
|
||||
step: 0.05
|
||||
)
|
||||
.tint(Color.Accent.primary)
|
||||
.tint(AppAccent.primary)
|
||||
|
||||
Image(systemName: "sun.max.fill")
|
||||
.font(.system(size: Design.BaseFontSize.large))
|
||||
@ -443,7 +458,7 @@ struct SettingsView: View {
|
||||
|
||||
Image(systemName: "crown.fill")
|
||||
.font(.system(size: Design.BaseFontSize.small))
|
||||
.foregroundStyle(Color.Status.warning)
|
||||
.foregroundStyle(AppStatus.warning)
|
||||
}
|
||||
|
||||
Text(String(localized: "Automatically keeps you centered in the frame"))
|
||||
@ -451,7 +466,7 @@ struct SettingsView: View {
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
}
|
||||
}
|
||||
.tint(Color.Accent.primary)
|
||||
.tint(AppAccent.primary)
|
||||
.padding(.vertical, Design.Spacing.xSmall)
|
||||
.disabled(!isPremiumUnlocked)
|
||||
.accessibilityLabel(String(localized: "Enable Center Stage"))
|
||||
@ -477,7 +492,7 @@ struct SettingsView: View {
|
||||
|
||||
Image(systemName: "crown.fill")
|
||||
.font(.system(size: Design.BaseFontSize.small))
|
||||
.foregroundStyle(Color.Status.warning)
|
||||
.foregroundStyle(AppStatus.warning)
|
||||
}
|
||||
|
||||
Text(isPremiumUnlocked
|
||||
@ -512,7 +527,7 @@ struct SettingsView: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(
|
||||
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)
|
||||
@ -536,7 +551,7 @@ struct SettingsView: View {
|
||||
HStack(spacing: Design.Spacing.medium) {
|
||||
Image(systemName: "crown.fill")
|
||||
.font(.title2)
|
||||
.foregroundStyle(Color.Status.warning)
|
||||
.foregroundStyle(AppStatus.warning)
|
||||
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
||||
Text(String(localized: "Upgrade to Pro"))
|
||||
@ -557,8 +572,8 @@ struct SettingsView: View {
|
||||
.padding(Design.Spacing.medium)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
|
||||
.fill(Color.Accent.primary.opacity(Design.Opacity.subtle))
|
||||
.strokeBorder(Color.Accent.primary.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.thin)
|
||||
.fill(AppAccent.primary.opacity(Design.Opacity.subtle))
|
||||
.strokeBorder(AppAccent.primary.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.thin)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@ -584,7 +599,7 @@ struct SettingsView: View {
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
}
|
||||
}
|
||||
.tint(Color.Accent.primary)
|
||||
.tint(AppAccent.primary)
|
||||
.padding(.vertical, Design.Spacing.xSmall)
|
||||
.disabled(!viewModel.iCloudAvailable)
|
||||
.accessibilityHint(String(localized: "Syncs settings across all your devices via iCloud"))
|
||||
@ -607,7 +622,7 @@ struct SettingsView: View {
|
||||
} label: {
|
||||
Text(String(localized: "Sync Now"))
|
||||
.font(.system(size: Design.BaseFontSize.caption, weight: .medium))
|
||||
.foregroundStyle(Color.Accent.primary)
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
}
|
||||
}
|
||||
.padding(.top, Design.Spacing.xSmall)
|
||||
@ -626,9 +641,9 @@ struct SettingsView: View {
|
||||
|
||||
private var syncStatusColor: Color {
|
||||
if !viewModel.hasCompletedInitialSync {
|
||||
return Color.Status.warning
|
||||
return AppStatus.warning
|
||||
}
|
||||
return Color.Status.success
|
||||
return AppStatus.success
|
||||
}
|
||||
|
||||
private var syncStatusText: String {
|
||||
@ -672,7 +687,7 @@ struct SettingsView: View {
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.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)
|
||||
}
|
||||
@ -703,7 +718,7 @@ struct SettingsView: View {
|
||||
|
||||
Image(systemName: "crown.fill")
|
||||
.font(.system(size: Design.BaseFontSize.small))
|
||||
.foregroundStyle(Color.Status.warning)
|
||||
.foregroundStyle(AppStatus.warning)
|
||||
}
|
||||
|
||||
Text(subtitle)
|
||||
@ -711,7 +726,7 @@ struct SettingsView: View {
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
}
|
||||
}
|
||||
.tint(Color.Accent.primary)
|
||||
.tint(AppAccent.primary)
|
||||
.padding(.vertical, Design.Spacing.xSmall)
|
||||
.disabled(!isPremiumUnlocked)
|
||||
.accessibilityHint(accessibilityHint)
|
||||
@ -744,7 +759,7 @@ struct SettingsView: View {
|
||||
|
||||
Image(systemName: "crown.fill")
|
||||
.font(.system(size: Design.BaseFontSize.small))
|
||||
.foregroundStyle(Color.Status.warning)
|
||||
.foregroundStyle(AppStatus.warning)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
@ -764,7 +779,7 @@ struct SettingsView: View {
|
||||
subtitle: "Unlock all premium features for testing",
|
||||
isOn: $viewModel.isDebugPremiumEnabled
|
||||
)
|
||||
.tint(Color.Status.warning)
|
||||
.tint(AppStatus.warning)
|
||||
// Icon Generator
|
||||
NavigationLink {
|
||||
IconGeneratorView(config: .selfieCam, appName: "SelfieCam")
|
||||
@ -787,7 +802,7 @@ struct SettingsView: View {
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.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)
|
||||
|
||||
@ -817,7 +832,7 @@ struct SettingsView: View {
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.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)
|
||||
}
|
||||
@ -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 {
|
||||
SettingsView(viewModel: SettingsViewModel(), showPaywall: .constant(false))
|
||||
.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