450 lines
17 KiB
Swift
450 lines
17 KiB
Swift
//
|
|
// UserDefaultsStore.swift
|
|
// Reflect (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 stacked = 2 // Full-width vertical list
|
|
case aura = 3 // Atmospheric glowing orbs with flowing layout
|
|
case orbit = 4 // Celestial orbit with center core
|
|
case neon = 5 // Synthwave arcade equalizer with glowing segments
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .horizontal: return "Horizontal"
|
|
case .cards: return "Cards"
|
|
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 // 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
|
|
}
|
|
|
|
/// Styles available in the picker (some are disabled/experimental)
|
|
var isAvailable: Bool {
|
|
switch self {
|
|
case .motion, .leather, .wave, .morph, .prism, .ink:
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
/// All styles available to users
|
|
static var availableCases: [DayViewStyle] {
|
|
allCases.filter { $0.isAvailable }
|
|
}
|
|
}
|
|
|
|
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 showNSFW
|
|
case shape
|
|
case daysFilter
|
|
case firstLaunchDate
|
|
case hasActiveSubscription
|
|
case cachedSubscriptionExpiration
|
|
case lastVotedDate
|
|
case votingLayoutStyle
|
|
case dayViewStyle
|
|
case privacyLockEnabled
|
|
case healthKitEnabled
|
|
case healthKitSyncEnabled
|
|
case paywallStyle
|
|
case lockScreenStyle
|
|
case celebrationAnimation
|
|
case hapticFeedbackEnabled
|
|
|
|
case contentViewCurrentSelectedHeaderViewBackDays
|
|
case contentViewHeaderTag
|
|
case contentViewHeaderTagViewOneViewType
|
|
case contentViewHeaderTagViewTwoViewType
|
|
case currentSelectedHeaderViewViewType
|
|
}
|
|
|
|
/// Cached onboarding data to avoid repeated JSON decoding
|
|
private static var cachedOnboardingData: OnboardingData?
|
|
|
|
static func getOnboarding() -> OnboardingData {
|
|
// Return cached data if available
|
|
if let cached = cachedOnboardingData {
|
|
return cached
|
|
}
|
|
|
|
// Decode and cache
|
|
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.savedOnboardingData.rawValue) as? Data,
|
|
let model = try? JSONDecoder().decode(OnboardingData.self, from: data) {
|
|
cachedOnboardingData = model
|
|
return model
|
|
} else {
|
|
let defaultData = OnboardingData()
|
|
cachedOnboardingData = defaultData
|
|
return defaultData
|
|
}
|
|
}
|
|
|
|
/// Invalidate cached onboarding data (call when data might have changed externally)
|
|
static func invalidateOnboardingCache() {
|
|
cachedOnboardingData = nil
|
|
}
|
|
|
|
@discardableResult
|
|
static func saveOnboarding(onboardingData: OnboardingData) -> OnboardingData {
|
|
// Invalidate cache before saving
|
|
cachedOnboardingData = nil
|
|
|
|
do {
|
|
let data = try JSONEncoder().encode(onboardingData)
|
|
GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.savedOnboardingData.rawValue)
|
|
} catch {
|
|
print("Error saving onboarding: \(error)")
|
|
}
|
|
|
|
// Re-cache the saved data
|
|
cachedOnboardingData = onboardingData
|
|
return onboardingData
|
|
}
|
|
|
|
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 {
|
|
// Try String value first (new format)
|
|
if let stringValue = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.theme.rawValue) as? String,
|
|
let model = Theme(rawValue: stringValue) {
|
|
return model
|
|
}
|
|
// Fall back to Int value (legacy format for existing users)
|
|
if let intValue = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.theme.rawValue) as? Int,
|
|
let model = Theme(legacyIntValue: intValue) {
|
|
// Migrate to new String format
|
|
GroupUserDefaults.groupDefaults.set(model.rawValue, forKey: UserDefaultsStore.Keys.theme.rawValue)
|
|
return model
|
|
}
|
|
return Theme.system
|
|
}
|
|
|
|
/// Call this on app launch to migrate any legacy Int-based theme values to String format
|
|
static func migrateThemeIfNeeded() {
|
|
// Check if we have an Int value stored (legacy format)
|
|
if let intValue = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.theme.rawValue) as? Int,
|
|
let model = Theme(legacyIntValue: intValue) {
|
|
// Migrate to new String format
|
|
GroupUserDefaults.groupDefaults.set(model.rawValue, forKey: UserDefaultsStore.Keys.theme.rawValue)
|
|
GroupUserDefaults.groupDefaults.synchronize()
|
|
}
|
|
}
|
|
|
|
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]
|
|
}
|
|
}
|
|
}
|
|
|