diff --git a/SelfieCam/Features/Settings/SettingsViewModel+Camera.swift b/SelfieCam/Features/Settings/SettingsViewModel+Camera.swift new file mode 100644 index 0000000..e0e9b89 --- /dev/null +++ b/SelfieCam/Features/Settings/SettingsViewModel+Camera.swift @@ -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 } + } + } +} diff --git a/SelfieCam/Features/Settings/SettingsViewModel+CloudSync.swift b/SelfieCam/Features/Settings/SettingsViewModel+CloudSync.swift new file mode 100644 index 0000000..0af8b05 --- /dev/null +++ b/SelfieCam/Features/Settings/SettingsViewModel+CloudSync.swift @@ -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() + } +} diff --git a/SelfieCam/Features/Settings/SettingsViewModel+Premium.swift b/SelfieCam/Features/Settings/SettingsViewModel+Premium.swift new file mode 100644 index 0000000..c88c130 --- /dev/null +++ b/SelfieCam/Features/Settings/SettingsViewModel+Premium.swift @@ -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() + } +} diff --git a/SelfieCam/Features/Settings/SettingsViewModel+RingLight.swift b/SelfieCam/Features/Settings/SettingsViewModel+RingLight.swift new file mode 100644 index 0000000..341820a --- /dev/null +++ b/SelfieCam/Features/Settings/SettingsViewModel+RingLight.swift @@ -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 + } +} diff --git a/SelfieCam/Features/Settings/SettingsViewModel.swift b/SelfieCam/Features/Settings/SettingsViewModel.swift index 7dca48f..d56e995 100644 --- a/SelfieCam/Features/Settings/SettingsViewModel.swift +++ b/SelfieCam/Features/Settings/SettingsViewModel.swift @@ -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,100 +35,58 @@ final class SettingsViewModel: RingLightConfigurable, CloudSyncable { /// Default ring border size static let defaultRingSize: CGFloat = 40 - // MARK: - Premium Manager - - /// Premium manager for checking subscription status - @ObservationIgnored private let premiumManager = PremiumManager() - - /// Whether the user has premium access (stored property for proper UI updates) - private var _isPremiumUnlocked: Bool = false - - /// Whether the user has premium access (observable for UI updates) - var isPremiumUnlocked: Bool { - get { _isPremiumUnlocked } - set { - _isPremiumUnlocked = newValue - } - } + // MARK: - Premium Constants - // MARK: - Cloud Sync Manager + /// Default free color ID for non-premium users + static let defaultFreeColorId = "pureWhite" + + /// Premium color IDs that require subscription + static let premiumColorIds: Set = ["iceBlue", "softPink", "warmAmber", "coolLavender", RingLightColor.customId] + + /// Premium timer options that require subscription + static let premiumTimerOptions: Set = [.five, .ten] + + /// Premium photo quality options that require subscription + static let premiumPhotoQualities: Set = [.high] + + // MARK: - Internal Dependencies + + /// Premium manager for checking subscription status + @ObservationIgnored let premiumManager = PremiumManager() /// Manages iCloud sync for settings across all devices - private let cloudSync = CloudSyncManager() + let cloudSync = CloudSyncManager() /// Debounce task for slider values - private var debounceTask: Task? + var debounceTask: Task? /// 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) - 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 } - } - } - } + var _cachedRingSize: CGFloat? /// 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 = ["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 } - } - } + var _cachedLightColorId: String? /// Cached custom color for immediate UI updates - private var _cachedCustomColor: Color? + 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: - Premium Status + + /// Whether the user has premium access (stored property for proper UI updates) + private var _isPremiumUnlocked: Bool = false + + /// Whether the user has premium access (observable for UI updates) + var isPremiumUnlocked: Bool { + get { _isPremiumUnlocked } + set { _isPremiumUnlocked = newValue } } + // MARK: - Display Settings + /// Whether the camera preview is flipped to show a true mirror (PREMIUM) var isMirrorFlipped: Bool { get { PremiumGate.get(cloudSync.data.isMirrorFlipped, default: false, isPremium: isPremiumUnlocked) } @@ -175,242 +117,26 @@ 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 = [.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 = [.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() { // Initialize premium status _isPremiumUnlocked = premiumManager.isPremiumUnlocked // 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 { diff --git a/SelfieCam/Shared/Models/TimerOption.swift b/SelfieCam/Shared/Models/TimerOption.swift new file mode 100644 index 0000000..2263c88 --- /dev/null +++ b/SelfieCam/Shared/Models/TimerOption.swift @@ -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 + } + } +}