Split SettingsViewModel into logical extensions
- TimerOption.swift - Timer enum moved to Models/ - SettingsViewModel+RingLight.swift - Ring light settings - SettingsViewModel+Camera.swift - Camera, flash, HDR, timer, quality - SettingsViewModel+Premium.swift - Debug toggle and premium reset - SettingsViewModel+CloudSync.swift - iCloud sync conformance - SettingsViewModel.swift - Core class with display/capture settings
This commit is contained in:
parent
a3d7370f32
commit
60ea419a5b
105
SelfieCam/Features/Settings/SettingsViewModel+Camera.swift
Normal file
105
SelfieCam/Features/Settings/SettingsViewModel+Camera.swift
Normal file
@ -0,0 +1,105 @@
|
||||
//
|
||||
// SettingsViewModel+Camera.swift
|
||||
// SelfieCam
|
||||
//
|
||||
// Created by Matt Bruce on 1/4/26.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Bedrock
|
||||
import MijickCamera
|
||||
|
||||
// MARK: - Camera Settings
|
||||
|
||||
extension SettingsViewModel {
|
||||
|
||||
// MARK: - Camera Position
|
||||
|
||||
/// Raw camera position string for comparison (avoids MijickCamera enum issues)
|
||||
var cameraPositionRaw: String {
|
||||
cloudSync.data.cameraPositionRaw
|
||||
}
|
||||
|
||||
var cameraPosition: CameraPosition {
|
||||
get {
|
||||
let raw = cloudSync.data.cameraPositionRaw
|
||||
let position: CameraPosition = raw == "front" ? .front : .back
|
||||
Design.debugLog("cameraPosition getter: raw='\(raw)' -> \(position)")
|
||||
return position
|
||||
}
|
||||
set {
|
||||
let rawValue = newValue == .front ? "front" : "back"
|
||||
Design.debugLog("cameraPosition setter: \(newValue) -> raw='\(rawValue)'")
|
||||
updateSettings { $0.cameraPositionRaw = rawValue }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Flash Mode
|
||||
|
||||
var flashMode: CameraFlashMode {
|
||||
get { CameraFlashMode(rawValue: cloudSync.data.flashModeRaw) ?? .off }
|
||||
set { updateSettings { $0.flashModeRaw = newValue.rawValue } }
|
||||
}
|
||||
|
||||
/// Whether flash is synced with ring light color (PREMIUM)
|
||||
var isFlashSyncedWithRingLight: Bool {
|
||||
get { PremiumGate.get(cloudSync.data.isFlashSyncedWithRingLight, default: false, isPremium: isPremiumUnlocked) }
|
||||
set {
|
||||
guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
|
||||
updateSettings { $0.isFlashSyncedWithRingLight = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - HDR Mode (Premium)
|
||||
|
||||
/// HDR mode setting (PREMIUM)
|
||||
var hdrMode: CameraHDRMode {
|
||||
get {
|
||||
let stored = CameraHDRMode(rawValue: cloudSync.data.hdrModeRaw) ?? .off
|
||||
return PremiumGate.get(stored, default: .off, isPremium: isPremiumUnlocked)
|
||||
}
|
||||
set {
|
||||
guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
|
||||
updateSettings { $0.hdrModeRaw = newValue.rawValue }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Photo Quality (High is Premium)
|
||||
|
||||
/// Photo quality setting (high is PREMIUM)
|
||||
var photoQuality: PhotoQuality {
|
||||
get {
|
||||
let stored = PhotoQuality(rawValue: cloudSync.data.photoQualityRaw) ?? PhotoQuality.high
|
||||
return PremiumGate.get(stored, default: .medium, premiumValues: Self.premiumPhotoQualities, isPremium: isPremiumUnlocked)
|
||||
}
|
||||
set {
|
||||
guard PremiumGate.canSet(newValue, premiumValues: Self.premiumPhotoQualities, isPremium: isPremiumUnlocked) else { return }
|
||||
updateSettings { $0.photoQualityRaw = newValue.rawValue }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Center Stage (Premium)
|
||||
|
||||
/// Whether Center Stage is enabled (PREMIUM)
|
||||
var isCenterStageEnabled: Bool {
|
||||
get { PremiumGate.get(cloudSync.data.isCenterStageEnabled, default: false, isPremium: isPremiumUnlocked) }
|
||||
set {
|
||||
guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
|
||||
updateSettings { $0.isCenterStageEnabled = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Timer (5s/10s are Premium)
|
||||
|
||||
/// Selected timer option (5s and 10s are PREMIUM)
|
||||
var selectedTimer: TimerOption {
|
||||
get {
|
||||
let stored = TimerOption(rawValue: cloudSync.data.selectedTimerRaw) ?? .off
|
||||
return PremiumGate.get(stored, default: .three, premiumValues: Self.premiumTimerOptions, isPremium: isPremiumUnlocked)
|
||||
}
|
||||
set {
|
||||
guard PremiumGate.canSet(newValue, premiumValues: Self.premiumTimerOptions, isPremium: isPremiumUnlocked) else { return }
|
||||
updateSettings { $0.selectedTimerRaw = newValue.rawValue }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
//
|
||||
// SettingsViewModel+CloudSync.swift
|
||||
// SelfieCam
|
||||
//
|
||||
// Created by Matt Bruce on 1/4/26.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Bedrock
|
||||
|
||||
// MARK: - CloudSyncable Conformance
|
||||
|
||||
extension SettingsViewModel: CloudSyncable {
|
||||
|
||||
/// Whether iCloud sync is available
|
||||
var iCloudAvailable: Bool { cloudSync.iCloudAvailable }
|
||||
|
||||
/// Whether iCloud sync is enabled (available to all users)
|
||||
var iCloudEnabled: Bool {
|
||||
get { cloudSync.iCloudEnabled }
|
||||
set { cloudSync.iCloudEnabled = newValue }
|
||||
}
|
||||
|
||||
/// Last sync date
|
||||
var lastSyncDate: Date? { cloudSync.lastSyncDate }
|
||||
|
||||
/// Current sync status message
|
||||
var syncStatus: String { cloudSync.syncStatus }
|
||||
|
||||
/// Whether initial sync has completed
|
||||
var hasCompletedInitialSync: Bool { cloudSync.hasCompletedInitialSync }
|
||||
|
||||
// MARK: - Sync Actions
|
||||
|
||||
/// Forces a sync with iCloud
|
||||
func forceSync() {
|
||||
cloudSync.sync()
|
||||
}
|
||||
|
||||
/// Resets all settings to defaults
|
||||
func resetToDefaults() {
|
||||
cloudSync.reset()
|
||||
}
|
||||
}
|
||||
83
SelfieCam/Features/Settings/SettingsViewModel+Premium.swift
Normal file
83
SelfieCam/Features/Settings/SettingsViewModel+Premium.swift
Normal file
@ -0,0 +1,83 @@
|
||||
//
|
||||
// SettingsViewModel+Premium.swift
|
||||
// SelfieCam
|
||||
//
|
||||
// Created by Matt Bruce on 1/4/26.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Bedrock
|
||||
|
||||
// MARK: - Premium Features
|
||||
|
||||
extension SettingsViewModel {
|
||||
|
||||
// MARK: - Debug Premium Toggle
|
||||
|
||||
/// Debug premium toggle for testing (DEBUG builds only)
|
||||
var isDebugPremiumEnabled: Bool {
|
||||
get { premiumManager.isDebugPremiumToggleEnabled }
|
||||
set {
|
||||
let wasPremium = premiumManager.isDebugPremiumToggleEnabled
|
||||
premiumManager.isDebugPremiumToggleEnabled = newValue
|
||||
|
||||
// Update premium status for UI refresh
|
||||
isPremiumUnlocked = premiumManager.isPremiumUnlocked
|
||||
|
||||
// Reset premium settings when toggling OFF
|
||||
if wasPremium && !newValue {
|
||||
resetPremiumSettingsToDefaults()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Premium Reset
|
||||
|
||||
/// Reset premium-only settings to their free defaults when debug premium is disabled
|
||||
func resetPremiumSettingsToDefaults() {
|
||||
// Clear cached values that depend on premium status
|
||||
_cachedRingSize = nil
|
||||
_cachedLightColorId = nil
|
||||
_cachedCustomColor = nil
|
||||
|
||||
// Reset premium settings to defaults (bypass premium gates for direct reset)
|
||||
updateSettings { settings in
|
||||
if settings.lightColorId != Self.defaultFreeColorId {
|
||||
settings.lightColorId = Self.defaultFreeColorId
|
||||
}
|
||||
|
||||
if settings.isMirrorFlipped {
|
||||
settings.isMirrorFlipped = false
|
||||
}
|
||||
|
||||
if settings.isSkinSmoothingEnabled {
|
||||
settings.isSkinSmoothingEnabled = false
|
||||
}
|
||||
|
||||
if settings.isFlashSyncedWithRingLight {
|
||||
settings.isFlashSyncedWithRingLight = false
|
||||
}
|
||||
|
||||
if settings.hdrModeRaw != CameraHDRMode.off.rawValue {
|
||||
settings.hdrModeRaw = CameraHDRMode.off.rawValue
|
||||
}
|
||||
|
||||
if settings.photoQualityRaw != PhotoQuality.medium.rawValue {
|
||||
settings.photoQualityRaw = PhotoQuality.medium.rawValue
|
||||
}
|
||||
|
||||
// Only reset timer if it's a premium option (5s or 10s)
|
||||
let currentTimer = TimerOption(rawValue: settings.selectedTimerRaw) ?? .three
|
||||
if Self.premiumTimerOptions.contains(currentTimer) {
|
||||
settings.selectedTimerRaw = TimerOption.three.rawValue
|
||||
}
|
||||
|
||||
if settings.isCenterStageEnabled {
|
||||
settings.isCenterStageEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
// Force sync to ensure changes are saved
|
||||
forceSync()
|
||||
}
|
||||
}
|
||||
113
SelfieCam/Features/Settings/SettingsViewModel+RingLight.swift
Normal file
113
SelfieCam/Features/Settings/SettingsViewModel+RingLight.swift
Normal file
@ -0,0 +1,113 @@
|
||||
//
|
||||
// SettingsViewModel+RingLight.swift
|
||||
// SelfieCam
|
||||
//
|
||||
// Created by Matt Bruce on 1/4/26.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Bedrock
|
||||
|
||||
// MARK: - Ring Light Settings
|
||||
|
||||
extension SettingsViewModel {
|
||||
|
||||
// MARK: - Ring Size
|
||||
|
||||
/// Ring border size in points (debounced save)
|
||||
var ringSize: CGFloat {
|
||||
get { _cachedRingSize ?? cloudSync.data.ringSize }
|
||||
set {
|
||||
_cachedRingSize = newValue
|
||||
debouncedSave(key: "ringSize") {
|
||||
self._cachedRingSize = nil
|
||||
self.updateSettings { $0.ringSize = newValue }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience property for border width (same as ringSize)
|
||||
var borderWidth: CGFloat { ringSize }
|
||||
|
||||
// MARK: - Ring Light Enabled/Opacity
|
||||
|
||||
var isRingLightEnabled: Bool {
|
||||
get { cloudSync.data.isRingLightEnabled }
|
||||
set { updateSettings { $0.isRingLightEnabled = newValue } }
|
||||
}
|
||||
|
||||
var ringLightOpacity: Double {
|
||||
get { cloudSync.data.ringLightOpacity }
|
||||
set { updateSettings { $0.ringLightOpacity = newValue } }
|
||||
}
|
||||
|
||||
// MARK: - Light Color
|
||||
|
||||
/// ID of the selected light color preset
|
||||
var lightColorId: String {
|
||||
get {
|
||||
let storedId = _cachedLightColorId ?? cloudSync.data.lightColorId
|
||||
return PremiumGate.get(storedId, default: Self.defaultFreeColorId, premiumValues: Self.premiumColorIds, isPremium: isPremiumUnlocked)
|
||||
}
|
||||
set {
|
||||
guard PremiumGate.canSet(newValue, premiumValues: Self.premiumColorIds, isPremium: isPremiumUnlocked) else { return }
|
||||
_cachedLightColorId = newValue
|
||||
updateSettings { $0.lightColorId = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom color for ring light (premium feature, debounced save)
|
||||
var customColor: Color {
|
||||
get {
|
||||
_cachedCustomColor ?? Color(
|
||||
red: cloudSync.data.customColorRed,
|
||||
green: cloudSync.data.customColorGreen,
|
||||
blue: cloudSync.data.customColorBlue
|
||||
)
|
||||
}
|
||||
set {
|
||||
guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
|
||||
_cachedCustomColor = newValue
|
||||
let rgb = CustomColorRGB(from: newValue)
|
||||
debouncedSave(key: "customColor") {
|
||||
self._cachedCustomColor = nil
|
||||
self.updateSettings {
|
||||
$0.customColorRed = rgb.red
|
||||
$0.customColorGreen = rgb.green
|
||||
$0.customColorBlue = rgb.blue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The currently selected light color preset
|
||||
var selectedLightColor: RingLightColor {
|
||||
get { RingLightColor.fromId(lightColorId, customColor: customColor) }
|
||||
set {
|
||||
lightColorId = newValue.id
|
||||
if newValue.isCustom {
|
||||
customColor = newValue.color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The actual color to use for the ring light
|
||||
var lightColor: Color {
|
||||
if lightColorId == RingLightColor.customId {
|
||||
return customColor
|
||||
}
|
||||
return selectedLightColor.color
|
||||
}
|
||||
|
||||
/// Whether custom color is currently selected
|
||||
var isCustomColorSelected: Bool {
|
||||
lightColorId == RingLightColor.customId
|
||||
}
|
||||
|
||||
/// Sets the custom color and selects it (PREMIUM)
|
||||
func selectCustomColor(_ color: Color) {
|
||||
guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
|
||||
customColor = color
|
||||
lightColorId = RingLightColor.customId
|
||||
}
|
||||
}
|
||||
@ -1,44 +1,28 @@
|
||||
//
|
||||
// SettingsViewModel.swift
|
||||
// SelfieCam
|
||||
//
|
||||
// Created by Matt Bruce on 1/4/26.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Bedrock
|
||||
import MijickCamera
|
||||
|
||||
// MARK: - Timer Options
|
||||
|
||||
enum TimerOption: String, CaseIterable, Identifiable {
|
||||
case off = "off"
|
||||
case three = "3"
|
||||
case five = "5"
|
||||
case ten = "10"
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .off: return String(localized: "Off")
|
||||
case .three: return String(localized: "3s")
|
||||
case .five: return String(localized: "5s")
|
||||
case .ten: return String(localized: "10s")
|
||||
}
|
||||
}
|
||||
|
||||
var seconds: Int {
|
||||
switch self {
|
||||
case .off: return 0
|
||||
case .three: return 3
|
||||
case .five: return 5
|
||||
case .ten: return 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Settings ViewModel
|
||||
|
||||
/// Observable settings view model with iCloud sync across all devices.
|
||||
/// Uses Bedrock's CloudSyncManager for automatic synchronization.
|
||||
/// Premium features are automatically reset to defaults when user doesn't have premium.
|
||||
///
|
||||
/// Extensions:
|
||||
/// - `SettingsViewModel+RingLight.swift` - Ring light settings
|
||||
/// - `SettingsViewModel+Camera.swift` - Camera settings (flash, HDR, timer, etc.)
|
||||
/// - `SettingsViewModel+Premium.swift` - Premium/debug toggle and reset logic
|
||||
/// - `SettingsViewModel+CloudSync.swift` - iCloud sync conformance
|
||||
@MainActor
|
||||
@Observable
|
||||
final class SettingsViewModel: RingLightConfigurable, CloudSyncable {
|
||||
final class SettingsViewModel: RingLightConfigurable {
|
||||
|
||||
// MARK: - Ring Size Limits
|
||||
|
||||
@ -51,10 +35,46 @@ final class SettingsViewModel: RingLightConfigurable, CloudSyncable {
|
||||
/// Default ring border size
|
||||
static let defaultRingSize: CGFloat = 40
|
||||
|
||||
// MARK: - Premium Manager
|
||||
// MARK: - Premium Constants
|
||||
|
||||
/// Default free color ID for non-premium users
|
||||
static let defaultFreeColorId = "pureWhite"
|
||||
|
||||
/// Premium color IDs that require subscription
|
||||
static let premiumColorIds: Set<String> = ["iceBlue", "softPink", "warmAmber", "coolLavender", RingLightColor.customId]
|
||||
|
||||
/// Premium timer options that require subscription
|
||||
static let premiumTimerOptions: Set<TimerOption> = [.five, .ten]
|
||||
|
||||
/// Premium photo quality options that require subscription
|
||||
static let premiumPhotoQualities: Set<PhotoQuality> = [.high]
|
||||
|
||||
// MARK: - Internal Dependencies
|
||||
|
||||
/// Premium manager for checking subscription status
|
||||
@ObservationIgnored private let premiumManager = PremiumManager()
|
||||
@ObservationIgnored let premiumManager = PremiumManager()
|
||||
|
||||
/// Manages iCloud sync for settings across all devices
|
||||
let cloudSync = CloudSyncManager<SyncedSettings>()
|
||||
|
||||
/// Debounce task for slider values
|
||||
var debounceTask: Task<Void, Never>?
|
||||
|
||||
/// Debounce delay for continuous slider updates (in seconds)
|
||||
private static let debounceDelay: Duration = .milliseconds(300)
|
||||
|
||||
// MARK: - Cached Values (for immediate UI updates)
|
||||
|
||||
/// Cached ring size for immediate UI updates (before debounced save)
|
||||
var _cachedRingSize: CGFloat?
|
||||
|
||||
/// Cached light color ID for immediate UI updates
|
||||
var _cachedLightColorId: String?
|
||||
|
||||
/// Cached custom color for immediate UI updates
|
||||
var _cachedCustomColor: Color?
|
||||
|
||||
// MARK: - Premium Status
|
||||
|
||||
/// Whether the user has premium access (stored property for proper UI updates)
|
||||
private var _isPremiumUnlocked: Bool = false
|
||||
@ -62,88 +82,10 @@ final class SettingsViewModel: RingLightConfigurable, CloudSyncable {
|
||||
/// Whether the user has premium access (observable for UI updates)
|
||||
var isPremiumUnlocked: Bool {
|
||||
get { _isPremiumUnlocked }
|
||||
set {
|
||||
_isPremiumUnlocked = newValue
|
||||
}
|
||||
set { _isPremiumUnlocked = newValue }
|
||||
}
|
||||
|
||||
// MARK: - Cloud Sync Manager
|
||||
|
||||
/// Manages iCloud sync for settings across all devices
|
||||
private let cloudSync = CloudSyncManager<SyncedSettings>()
|
||||
|
||||
/// Debounce task for slider values
|
||||
private var debounceTask: Task<Void, Never>?
|
||||
|
||||
/// Debounce delay for continuous slider updates (in seconds)
|
||||
private static let debounceDelay: Duration = .milliseconds(300)
|
||||
|
||||
/// Cached ring size for immediate UI updates (before debounced save)
|
||||
private var _cachedRingSize: CGFloat?
|
||||
|
||||
// MARK: - Observable Properties (Synced)
|
||||
|
||||
/// Ring border size in points (debounced save)
|
||||
var ringSize: CGFloat {
|
||||
get { _cachedRingSize ?? cloudSync.data.ringSize }
|
||||
set {
|
||||
_cachedRingSize = newValue
|
||||
debouncedSave(key: "ringSize") {
|
||||
self._cachedRingSize = nil
|
||||
self.updateSettings { $0.ringSize = newValue }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cached light color ID for immediate UI updates
|
||||
private var _cachedLightColorId: String?
|
||||
|
||||
/// Default free color ID for non-premium users
|
||||
private static let defaultFreeColorId = "pureWhite"
|
||||
|
||||
/// Premium color IDs that require subscription
|
||||
private static let premiumColorIds: Set<String> = ["iceBlue", "softPink", "warmAmber", "coolLavender", RingLightColor.customId]
|
||||
|
||||
/// ID of the selected light color preset
|
||||
var lightColorId: String {
|
||||
get {
|
||||
let storedId = _cachedLightColorId ?? cloudSync.data.lightColorId
|
||||
return PremiumGate.get(storedId, default: Self.defaultFreeColorId, premiumValues: Self.premiumColorIds, isPremium: isPremiumUnlocked)
|
||||
}
|
||||
set {
|
||||
guard PremiumGate.canSet(newValue, premiumValues: Self.premiumColorIds, isPremium: isPremiumUnlocked) else { return }
|
||||
_cachedLightColorId = newValue
|
||||
updateSettings { $0.lightColorId = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
/// Cached custom color for immediate UI updates
|
||||
private var _cachedCustomColor: Color?
|
||||
|
||||
/// Custom color for ring light (premium feature, debounced save)
|
||||
/// Note: Getter always returns stored value to preserve user's choice if they re-subscribe
|
||||
var customColor: Color {
|
||||
get {
|
||||
_cachedCustomColor ?? Color(
|
||||
red: cloudSync.data.customColorRed,
|
||||
green: cloudSync.data.customColorGreen,
|
||||
blue: cloudSync.data.customColorBlue
|
||||
)
|
||||
}
|
||||
set {
|
||||
guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
|
||||
_cachedCustomColor = newValue
|
||||
let rgb = CustomColorRGB(from: newValue)
|
||||
debouncedSave(key: "customColor") {
|
||||
self._cachedCustomColor = nil
|
||||
self.updateSettings {
|
||||
$0.customColorRed = rgb.red
|
||||
$0.customColorGreen = rgb.green
|
||||
$0.customColorBlue = rgb.blue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// MARK: - Display Settings
|
||||
|
||||
/// Whether the camera preview is flipped to show a true mirror (PREMIUM)
|
||||
var isMirrorFlipped: Bool {
|
||||
@ -175,230 +117,14 @@ final class SettingsViewModel: RingLightConfigurable, CloudSyncable {
|
||||
set { updateSettings { $0.currentZoomFactor = newValue } }
|
||||
}
|
||||
|
||||
// MARK: - Capture Settings
|
||||
|
||||
/// Whether captures are auto-saved to Photo Library
|
||||
var isAutoSaveEnabled: Bool {
|
||||
get { cloudSync.data.isAutoSaveEnabled }
|
||||
set { updateSettings { $0.isAutoSaveEnabled = newValue } }
|
||||
}
|
||||
|
||||
// MARK: - Computed Properties
|
||||
|
||||
/// Convenience property for border width (same as ringSize)
|
||||
var borderWidth: CGFloat { ringSize }
|
||||
|
||||
/// Premium timer options that require subscription
|
||||
private static let premiumTimerOptions: Set<TimerOption> = [.five, .ten]
|
||||
|
||||
/// Selected timer option (5s and 10s are PREMIUM)
|
||||
var selectedTimer: TimerOption {
|
||||
get {
|
||||
let stored = TimerOption(rawValue: cloudSync.data.selectedTimerRaw) ?? .off
|
||||
return PremiumGate.get(stored, default: .three, premiumValues: Self.premiumTimerOptions, isPremium: isPremiumUnlocked)
|
||||
}
|
||||
set {
|
||||
guard PremiumGate.canSet(newValue, premiumValues: Self.premiumTimerOptions, isPremium: isPremiumUnlocked) else { return }
|
||||
updateSettings { $0.selectedTimerRaw = newValue.rawValue }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Camera Settings
|
||||
|
||||
var flashMode: CameraFlashMode {
|
||||
get { CameraFlashMode(rawValue: cloudSync.data.flashModeRaw) ?? .off }
|
||||
set { updateSettings { $0.flashModeRaw = newValue.rawValue } }
|
||||
}
|
||||
|
||||
/// Whether flash is synced with ring light color (PREMIUM)
|
||||
var isFlashSyncedWithRingLight: Bool {
|
||||
get { PremiumGate.get(cloudSync.data.isFlashSyncedWithRingLight, default: false, isPremium: isPremiumUnlocked) }
|
||||
set {
|
||||
guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
|
||||
updateSettings { $0.isFlashSyncedWithRingLight = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
/// HDR mode setting (PREMIUM)
|
||||
var hdrMode: CameraHDRMode {
|
||||
get {
|
||||
let stored = CameraHDRMode(rawValue: cloudSync.data.hdrModeRaw) ?? .off
|
||||
return PremiumGate.get(stored, default: .off, isPremium: isPremiumUnlocked)
|
||||
}
|
||||
set {
|
||||
guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
|
||||
updateSettings { $0.hdrModeRaw = newValue.rawValue }
|
||||
}
|
||||
}
|
||||
|
||||
/// Premium photo quality options that require subscription
|
||||
private static let premiumPhotoQualities: Set<PhotoQuality> = [.high]
|
||||
|
||||
/// Photo quality setting (high is PREMIUM)
|
||||
var photoQuality: PhotoQuality {
|
||||
get {
|
||||
let stored = PhotoQuality(rawValue: cloudSync.data.photoQualityRaw) ?? PhotoQuality.high
|
||||
return PremiumGate.get(stored, default: .medium, premiumValues: Self.premiumPhotoQualities, isPremium: isPremiumUnlocked)
|
||||
}
|
||||
set {
|
||||
guard PremiumGate.canSet(newValue, premiumValues: Self.premiumPhotoQualities, isPremium: isPremiumUnlocked) else { return }
|
||||
updateSettings { $0.photoQualityRaw = newValue.rawValue }
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw camera position string for comparison (avoids MijickCamera enum issues)
|
||||
var cameraPositionRaw: String {
|
||||
cloudSync.data.cameraPositionRaw
|
||||
}
|
||||
|
||||
var cameraPosition: CameraPosition {
|
||||
get {
|
||||
let raw = cloudSync.data.cameraPositionRaw
|
||||
let position: CameraPosition = raw == "front" ? .front : .back
|
||||
Design.debugLog("cameraPosition getter: raw='\(raw)' -> \(position)")
|
||||
return position
|
||||
}
|
||||
set {
|
||||
let rawValue = newValue == .front ? "front" : "back"
|
||||
Design.debugLog("cameraPosition setter: \(newValue) -> raw='\(rawValue)'")
|
||||
updateSettings { $0.cameraPositionRaw = rawValue }
|
||||
}
|
||||
}
|
||||
|
||||
var isRingLightEnabled: Bool {
|
||||
get { cloudSync.data.isRingLightEnabled }
|
||||
set { updateSettings { $0.isRingLightEnabled = newValue } }
|
||||
}
|
||||
|
||||
var ringLightOpacity: Double {
|
||||
get { cloudSync.data.ringLightOpacity }
|
||||
set { updateSettings { $0.ringLightOpacity = newValue } }
|
||||
}
|
||||
|
||||
/// Whether Center Stage is enabled (PREMIUM)
|
||||
var isCenterStageEnabled: Bool {
|
||||
get { PremiumGate.get(cloudSync.data.isCenterStageEnabled, default: false, isPremium: isPremiumUnlocked) }
|
||||
set {
|
||||
guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
|
||||
updateSettings { $0.isCenterStageEnabled = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
var selectedLightColor: RingLightColor {
|
||||
get { RingLightColor.fromId(lightColorId, customColor: customColor) }
|
||||
set {
|
||||
// Premium check handled by lightColorId and customColor setters
|
||||
lightColorId = newValue.id
|
||||
if newValue.isCustom {
|
||||
customColor = newValue.color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var lightColor: Color {
|
||||
if lightColorId == RingLightColor.customId {
|
||||
return customColor
|
||||
}
|
||||
return selectedLightColor.color
|
||||
}
|
||||
|
||||
/// Whether custom color is currently selected
|
||||
var isCustomColorSelected: Bool {
|
||||
lightColorId == RingLightColor.customId
|
||||
}
|
||||
|
||||
// MARK: - Debug Premium Toggle
|
||||
|
||||
/// Debug premium toggle for testing (DEBUG builds only)
|
||||
var isDebugPremiumEnabled: Bool {
|
||||
get { premiumManager.isDebugPremiumToggleEnabled }
|
||||
set {
|
||||
let wasPremium = premiumManager.isDebugPremiumToggleEnabled
|
||||
premiumManager.isDebugPremiumToggleEnabled = newValue
|
||||
|
||||
// Update premium status for UI refresh
|
||||
isPremiumUnlocked = premiumManager.isPremiumUnlocked
|
||||
|
||||
// Reset premium settings when toggling OFF
|
||||
if wasPremium && !newValue {
|
||||
resetPremiumSettingsToDefaults()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset premium-only settings to their free defaults when debug premium is disabled
|
||||
private func resetPremiumSettingsToDefaults() {
|
||||
// Clear cached values that depend on premium status
|
||||
_cachedRingSize = nil
|
||||
_cachedLightColorId = nil
|
||||
_cachedCustomColor = nil
|
||||
|
||||
// Reset premium settings to defaults (bypass premium gates for direct reset)
|
||||
updateSettings { settings in
|
||||
if settings.lightColorId != Self.defaultFreeColorId {
|
||||
settings.lightColorId = Self.defaultFreeColorId
|
||||
}
|
||||
|
||||
if settings.isMirrorFlipped {
|
||||
settings.isMirrorFlipped = false
|
||||
}
|
||||
|
||||
if settings.isSkinSmoothingEnabled {
|
||||
settings.isSkinSmoothingEnabled = false
|
||||
}
|
||||
|
||||
if settings.isFlashSyncedWithRingLight {
|
||||
settings.isFlashSyncedWithRingLight = false
|
||||
}
|
||||
|
||||
if settings.hdrModeRaw != CameraHDRMode.off.rawValue {
|
||||
settings.hdrModeRaw = CameraHDRMode.off.rawValue
|
||||
}
|
||||
|
||||
if settings.photoQualityRaw != PhotoQuality.medium.rawValue {
|
||||
settings.photoQualityRaw = PhotoQuality.medium.rawValue
|
||||
}
|
||||
|
||||
// Only reset timer if it's a premium option (5s or 10s)
|
||||
let currentTimer = TimerOption(rawValue: settings.selectedTimerRaw) ?? .three
|
||||
if Self.premiumTimerOptions.contains(currentTimer) {
|
||||
settings.selectedTimerRaw = TimerOption.three.rawValue
|
||||
}
|
||||
|
||||
if settings.isCenterStageEnabled {
|
||||
settings.isCenterStageEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
// Force sync to ensure changes are saved
|
||||
forceSync()
|
||||
}
|
||||
|
||||
/// Sets the custom color and selects it (PREMIUM)
|
||||
func selectCustomColor(_ color: Color) {
|
||||
guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
|
||||
customColor = color
|
||||
lightColorId = RingLightColor.customId
|
||||
}
|
||||
|
||||
// MARK: - Sync Status
|
||||
|
||||
/// Whether iCloud sync is available
|
||||
var iCloudAvailable: Bool { cloudSync.iCloudAvailable }
|
||||
|
||||
/// Whether iCloud sync is enabled (available to all users)
|
||||
var iCloudEnabled: Bool {
|
||||
get { cloudSync.iCloudEnabled }
|
||||
set { cloudSync.iCloudEnabled = newValue }
|
||||
}
|
||||
|
||||
/// Last sync date
|
||||
var lastSyncDate: Date? { cloudSync.lastSyncDate }
|
||||
|
||||
/// Current sync status message
|
||||
var syncStatus: String { cloudSync.syncStatus }
|
||||
|
||||
/// Whether initial sync has completed
|
||||
var hasCompletedInitialSync: Bool { cloudSync.hasCompletedInitialSync }
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init() {
|
||||
@ -407,10 +133,10 @@ final class SettingsViewModel: RingLightConfigurable, CloudSyncable {
|
||||
// CloudSyncManager handles syncing automatically
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
// MARK: - Internal Methods
|
||||
|
||||
/// Updates settings and saves to cloud immediately
|
||||
private func updateSettings(_ transform: (inout SyncedSettings) -> Void) {
|
||||
func updateSettings(_ transform: (inout SyncedSettings) -> Void) {
|
||||
cloudSync.update { settings in
|
||||
transform(&settings)
|
||||
settings.modificationCount += 1
|
||||
@ -418,7 +144,7 @@ final class SettingsViewModel: RingLightConfigurable, CloudSyncable {
|
||||
}
|
||||
|
||||
/// Debounces save operations for continuous values like sliders
|
||||
private func debouncedSave(key: String, action: @escaping () -> Void) {
|
||||
func debouncedSave(key: String, action: @escaping () -> Void) {
|
||||
// Cancel any pending debounce
|
||||
debounceTask?.cancel()
|
||||
|
||||
@ -432,18 +158,6 @@ final class SettingsViewModel: RingLightConfigurable, CloudSyncable {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sync Actions
|
||||
|
||||
/// Forces a sync with iCloud
|
||||
func forceSync() {
|
||||
cloudSync.sync()
|
||||
}
|
||||
|
||||
/// Resets all settings to defaults
|
||||
func resetToDefaults() {
|
||||
cloudSync.reset()
|
||||
}
|
||||
|
||||
// MARK: - Validation
|
||||
|
||||
var isValidConfiguration: Bool {
|
||||
|
||||
36
SelfieCam/Shared/Models/TimerOption.swift
Normal file
36
SelfieCam/Shared/Models/TimerOption.swift
Normal file
@ -0,0 +1,36 @@
|
||||
//
|
||||
// TimerOption.swift
|
||||
// SelfieCam
|
||||
//
|
||||
// Created by Matt Bruce on 1/4/26.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Self-timer duration options
|
||||
enum TimerOption: String, CaseIterable, Identifiable {
|
||||
case off = "off"
|
||||
case three = "3"
|
||||
case five = "5"
|
||||
case ten = "10"
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .off: return String(localized: "Off")
|
||||
case .three: return String(localized: "3s")
|
||||
case .five: return String(localized: "5s")
|
||||
case .ten: return String(localized: "10s")
|
||||
}
|
||||
}
|
||||
|
||||
var seconds: Int {
|
||||
switch self {
|
||||
case .off: return 0
|
||||
case .three: return 3
|
||||
case .five: return 5
|
||||
case .ten: return 10
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user