Files
Reflect/Shared/Models/UserDefaultsStore.swift
Trey t a0b30d8bae Add 12 cohesive app themes with matching subscription and lock screens
- Create AppTheme enum bundling color tint, icon pack, entry style, voting layout, paywall style, and lock screen style into unified themes
- Add AppThemePickerView for selecting themes with preview cards and detail sheets
- Extend PaywallStyle to 12 variants (celestial, garden, neon, minimal, zen, editorial, mixtape, heartfelt, luxe, forecast, playful, journal)
- Add LockScreenStyle enum with 13 variants including aurora default
- Create themed subscription paywalls with unique backgrounds, decorative elements, and typography for each style
- Create themed lock screens with unique backgrounds, central elements, and unlock buttons
- Update FeelsSubscriptionStoreView to read style from AppStorage so it auto-matches current theme
- Update PaywallPreviewSettingsView to support all 12 paywall styles

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 23:53:35 -06:00

394 lines
15 KiB
Swift

//
// UserDefaultsStore.swift
// Feels (iOS)
//
// Created by Trey Tartt on 1/22/22.
//
import Foundation
enum VotingLayoutStyle: Int, CaseIterable {
case horizontal = 0 // Current: 5 buttons in a row
case cards = 1 // Larger tappable cards with labels
case radial = 2 // Semi-circle/wheel arrangement
case stacked = 3 // Full-width vertical list
case aura = 4 // Atmospheric glowing orbs with flowing layout
case orbit = 5 // Celestial orbit with center core
case neon = 6 // Synthwave arcade equalizer with glowing segments
var displayName: String {
switch self {
case .horizontal: return "Horizontal"
case .cards: return "Cards"
case .radial: return "Radial"
case .stacked: return "Stacked"
case .aura: return "Aura"
case .orbit: return "Orbit"
case .neon: return "Neon"
}
}
}
enum PaywallStyle: Int, CaseIterable {
case celestial = 0 // Celestial Self-Discovery - aurora, floating orbs
case garden = 1 // Garden Growth - organic, blooming nature
case neon = 2 // Neon Pulse - synthwave, energetic
case minimal = 3 // Minimal Zen - clean, sophisticated
case zen = 4 // Zen Garden - ink brushstrokes, meditation
case editorial = 5 // Editorial - magazine typography
case mixtape = 6 // Mixtape - cassette/retro analog
case heartfelt = 7 // Heartfelt - hearts and emotion
case luxe = 8 // Luxe - premium glass materials
case forecast = 9 // Forecast - weather metaphors
case playful = 10 // Playful - vibrant emoji patterns
case journal = 11 // Journal - handwritten paper
var displayName: String {
switch self {
case .celestial: return "Celestial"
case .garden: return "Garden"
case .neon: return "Neon"
case .minimal: return "Minimal"
case .zen: return "Zen"
case .editorial: return "Editorial"
case .mixtape: return "Mixtape"
case .heartfelt: return "Heartfelt"
case .luxe: return "Luxe"
case .forecast: return "Forecast"
case .playful: return "Playful"
case .journal: return "Journal"
}
}
var description: String {
switch self {
case .celestial: return "Aurora lights & floating emotion orbs"
case .garden: return "Blooming flowers & organic growth"
case .neon: return "Synthwave energy & glowing pulses"
case .minimal: return "Clean typography & subtle elegance"
case .zen: return "Ink brushstrokes & meditative calm"
case .editorial: return "Magazine typography & literary elegance"
case .mixtape: return "Cassette tapes & analog nostalgia"
case .heartfelt: return "Hearts & emotional expression"
case .luxe: return "Premium glass & refined materials"
case .forecast: return "Weather metaphors & natural flow"
case .playful: return "Vibrant patterns & playful energy"
case .journal: return "Handwritten notes & paper textures"
}
}
}
enum LockScreenStyle: Int, CaseIterable {
case aurora = 0 // Default - emotional aurora with breathing orb
case zen = 1 // Zen - ink circles, minimal, calming
case neon = 2 // Neon - synthwave grid, glowing elements
case celestial = 3 // Celestial - stars, moon phases, cosmic
case editorial = 4 // Editorial - typography focused, clean
case mixtape = 5 // Mixtape - cassette aesthetic, retro
case bloom = 6 // Bloom - organic shapes, flowers
case heartfelt = 7 // Heartfelt - hearts, soft gradients
case minimal = 8 // Minimal - ultra clean, simple
case luxe = 9 // Luxe - glass, premium materials
case forecast = 10 // Forecast - weather, atmospheric
case playful = 11 // Playful - patterns, vibrant
case journal = 12 // Journal - paper, handwritten feel
var displayName: String {
switch self {
case .aurora: return "Aurora"
case .zen: return "Zen"
case .neon: return "Neon"
case .celestial: return "Celestial"
case .editorial: return "Editorial"
case .mixtape: return "Mixtape"
case .bloom: return "Bloom"
case .heartfelt: return "Heartfelt"
case .minimal: return "Minimal"
case .luxe: return "Luxe"
case .forecast: return "Forecast"
case .playful: return "Playful"
case .journal: return "Journal"
}
}
}
enum DayViewStyle: Int, CaseIterable {
case classic = 0 // Current card style with gradient icons
case minimal = 1 // Clean, simple flat cards
case compact = 2 // Dense timeline view
case bubble = 3 // Colorful full-width bubbles
case grid = 4 // 3 entries per row grid
case aura = 5 // Atmospheric glowing entries with giant typography
case chronicle = 6 // Editorial magazine with dramatic serif typography
case neon = 7 // Cyberpunk synthwave with glowing edges
case ink = 8 // Japanese zen calligraphy with brush strokes
case prism = 9 // Premium glassmorphism with light refraction
case tape = 10 // Retro cassette mixtape aesthetic
case morph = 11 // Liquid organic blob shapes
case stack = 12 // Layered paper notes with depth
case wave = 13 // Horizontal gradient river bands
case pattern = 14 // Mood icons as repeating background pattern
case leather = 15 // Skeuomorphic leather with stitching
case glass = 16 // iOS 26 liquid glass with variable blur
case motion = 17 // Accelerometer-driven parallax effect
case micro = 18 // Ultra compact single-line entries
case orbit = 19 // Celestial circular orbital arrangement
var displayName: String {
switch self {
case .classic: return "Classic"
case .minimal: return "Minimal"
case .compact: return "Compact"
case .bubble: return "Bubble"
case .grid: return "Grid"
case .aura: return "Aura"
case .chronicle: return "Chronicle"
case .neon: return "Neon"
case .ink: return "Ink"
case .prism: return "Prism"
case .tape: return "Tape"
case .morph: return "Morph"
case .stack: return "Stack"
case .wave: return "Wave"
case .pattern: return "Pattern"
case .leather: return "Leather"
case .glass: return "Glass"
case .motion: return "Motion"
case .micro: return "Micro"
case .orbit: return "Orbit"
}
}
var isGridLayout: Bool {
self == .grid
}
}
class UserDefaultsStore {
enum Keys: String {
case savedOnboardingData
case needsOnboarding
case useCloudKit
case deleteEnable
case mainViewTopHeaderIndex
case theme
case moodImages
case moodTint
case personalityPack
case customWidget
case customMoodTint
case customMoodTintUpdateNumber
case textColor
case showNSFW
case shape
case daysFilter
case firstLaunchDate
case hasActiveSubscription
case lastVotedDate
case votingLayoutStyle
case dayViewStyle
case privacyLockEnabled
case healthKitEnabled
case healthKitSyncEnabled
case paywallStyle
case lockScreenStyle
case contentViewCurrentSelectedHeaderViewBackDays
case contentViewHeaderTag
case contentViewHeaderTagViewOneViewType
case contentViewHeaderTagViewTwoViewType
case currentSelectedHeaderViewViewType
}
static func getOnboarding() -> OnboardingData {
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.savedOnboardingData.rawValue) as? Data,
let model = try? JSONDecoder().decode(OnboardingData.self, from: data) {
return model
} else {
return OnboardingData()
}
}
@discardableResult
static func saveOnboarding(onboardingData: OnboardingData) -> OnboardingData {
do {
let data = try JSONEncoder().encode(onboardingData)
GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.savedOnboardingData.rawValue)
} catch {
print("Error saving onboarding: \(error)")
}
return UserDefaultsStore.getOnboarding()
}
static func moodMoodImagable() -> MoodImagable.Type {
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.moodImages.rawValue) as? Int,
let model = MoodImages.init(rawValue: data) {
return model.moodImages
} else {
return MoodImages.FontAwesome.moodImages
}
}
static func moodTintable() -> MoodTintable.Type {
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.moodTint.rawValue) as? Int,
let model = MoodTints.init(rawValue: data) {
return model.moodTints
} else {
return MoodTints.Default.moodTints
}
}
static func personalityPackable() -> PersonalityPack {
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.personalityPack.rawValue) as? Int,
let model = PersonalityPack.init(rawValue: data) {
return model
} else {
return PersonalityPack.Default
}
}
static func theme() -> Theme {
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.theme.rawValue) as? Int,
let model = Theme.init(rawValue: data) {
return model
} else {
return Theme.system
}
}
static func getCustomWidgets() -> [CustomWidgetModel] {
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.customWidget.rawValue) as? Data,
let model = try? JSONDecoder().decode([CustomWidgetModel].self, from: data) {
return model
} else {
GroupUserDefaults.groupDefaults.removeObject(forKey: UserDefaultsStore.Keys.customWidget.rawValue)
let widget = CustomWidgetModel.randomWidget
widget.isSaved = true
let widgets = [widget]
guard let data = try? JSONEncoder().encode(widgets) else {
return widgets
}
GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.customWidget.rawValue)
if let savedData = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.customWidget.rawValue) as? Data,
let models = try? JSONDecoder().decode([CustomWidgetModel].self, from: savedData) {
return models.sorted { $0.createdDate < $1.createdDate }
} else {
return widgets
}
}
}
@discardableResult
static func saveCustomWidget(widgetModel: CustomWidgetModel, inUse: Bool) -> [CustomWidgetModel] {
do {
var existingWidgets = getCustomWidgets()
if let exisitingWidget = existingWidgets.firstIndex(where: {
$0.uuid == widgetModel.uuid
}) {
existingWidgets.remove(at: exisitingWidget)
// give it differnet uuid so the view updates
widgetModel.uuid = UUID().uuidString
}
if inUse {
existingWidgets.forEach({
$0.inUse = false
})
widgetModel.inUse = true
}
existingWidgets.append(widgetModel)
existingWidgets.forEach({
$0.isSaved = true
})
let data = try JSONEncoder().encode(existingWidgets)
GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.customWidget.rawValue)
} catch {
print("Error saving custom widget: \(error)")
}
return UserDefaultsStore.getCustomWidgets()
}
@discardableResult
static func deleteCustomWidget(withUUID uuid: String) -> [CustomWidgetModel] {
do {
var existingWidgets = getCustomWidgets()
if let exisitingWidget = existingWidgets.firstIndex(where: {
$0.uuid == uuid
}) {
existingWidgets.remove(at: exisitingWidget)
}
if existingWidgets.count == 0 {
let widget = CustomWidgetModel.randomWidget
widget.isSaved = true
widget.inUse = true
existingWidgets.append(widget)
}
if existingWidgets.first(where: { $0.inUse == true }) == nil {
existingWidgets.first?.inUse = true
}
let data = try JSONEncoder().encode(existingWidgets)
GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.customWidget.rawValue)
} catch {
print("Error deleting custom widget: \(error)")
}
return UserDefaultsStore.getCustomWidgets()
}
static func getCustomMoodTint() -> SavedMoodTint {
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.customMoodTint.rawValue) as? Data{
do {
let model = try JSONDecoder().decode(SavedMoodTint.self, from: data)
return model
} catch {
print(error)
}
}
return SavedMoodTint()
}
static func getCustomBGShape() -> BGShape {
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.shape.rawValue) as? Int,
let model = BGShape.init(rawValue: data) {
return model
} else {
return BGShape.circle
}
}
@discardableResult
static func saveCustomMoodTint(customTint: SavedMoodTint) -> SavedMoodTint {
do {
let data = try JSONEncoder().encode(customTint)
GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.customMoodTint.rawValue)
} catch {
print("Error saving custom mood tint: \(error)")
}
return UserDefaultsStore.getCustomMoodTint()
}
@discardableResult
static func saveDaysFilter(days: [Int]) -> [Int] {
GroupUserDefaults.groupDefaults.set(days, forKey: UserDefaultsStore.Keys.daysFilter.rawValue)
return UserDefaultsStore.getDaysFilter()
}
static func getDaysFilter() -> [Int] {
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.daysFilter.rawValue) as? [Int] {
return data
} else {
return [1,2,3,4,5,6,7]
}
}
}