- Add HealthKit State of Mind sync for mood entries - Add Live Activity with streak display and rating time window - Add App Shortcuts/Siri integration for voice mood logging - Add TipKit hints for feature discovery - Add centralized MoodLogger for consistent side effects - Add reminder time setting in Settings with time picker - Fix duplicate notifications when changing reminder time - Fix Live Activity streak showing 0 when not yet rated today - Fix slow tap response in entry detail mood selection - Update widget timeline to refresh at rating time - Sync widgets when reminder time changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
299 lines
11 KiB
Swift
299 lines
11 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
|
|
|
|
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"
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
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"
|
|
}
|
|
}
|
|
|
|
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 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]
|
|
}
|
|
}
|
|
}
|
|
|