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>
This commit is contained in:
295
Shared/Models/AppTheme.swift
Normal file
295
Shared/Models/AppTheme.swift
Normal file
@@ -0,0 +1,295 @@
|
||||
//
|
||||
// AppTheme.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Claude Code on 12/26/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// Cohesive themes that bundle colors, icons, entry styles, and voting layouts
|
||||
/// into unified aesthetic experiences. Each theme is designed around a specific
|
||||
/// emotional resonance and target user persona.
|
||||
enum AppTheme: Int, CaseIterable, Identifiable {
|
||||
case zenGarden = 0
|
||||
case synthwave = 1
|
||||
case celestial = 2
|
||||
case editorial = 3
|
||||
case mixtape = 4
|
||||
case bloom = 5
|
||||
case heartfelt = 6
|
||||
case minimal = 7
|
||||
case luxe = 8
|
||||
case forecast = 9
|
||||
case playful = 10
|
||||
case journal = 11
|
||||
|
||||
var id: Int { rawValue }
|
||||
|
||||
// MARK: - Display Properties
|
||||
|
||||
var name: String {
|
||||
switch self {
|
||||
case .zenGarden: return "Zen Garden"
|
||||
case .synthwave: return "Synthwave"
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
var tagline: String {
|
||||
switch self {
|
||||
case .zenGarden: return "Meditative calm"
|
||||
case .synthwave: return "Retro-futuristic energy"
|
||||
case .celestial: return "Cosmic wisdom"
|
||||
case .editorial: return "Literary elegance"
|
||||
case .mixtape: return "Analog nostalgia"
|
||||
case .bloom: return "Growth & healing"
|
||||
case .heartfelt: return "Emotional depth"
|
||||
case .minimal: return "Pure simplicity"
|
||||
case .luxe: return "Premium refinement"
|
||||
case .forecast: return "Mood as weather"
|
||||
case .playful: return "Fun & vibrant"
|
||||
case .journal: return "Personal diary"
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .zenGarden:
|
||||
return "Japanese minimalism meets mindful awareness. Soft pastels, organic growth icons, brush-stroke entries, and contemplative vertical voting."
|
||||
case .synthwave:
|
||||
return "80s arcade aesthetic with neon glow. Electric colors, cosmic icons, grid backgrounds, and equalizer-bar voting."
|
||||
case .celestial:
|
||||
return "Journey from void to starlight. Moon phases, orbital layouts, and planetary arrangements for cosmic mood tracking."
|
||||
case .editorial:
|
||||
return "Magazine-quality typography and layout. Clean icons, pull-quote entries, and sophisticated presentation."
|
||||
case .mixtape:
|
||||
return "Cassette culture and analog warmth. Tape reels, track numbers, and the tactile feel of pressing play."
|
||||
case .bloom:
|
||||
return "From wilted flower to full bloom. Organic shapes, glowing orbs, and the gentle metaphor of growth."
|
||||
case .heartfelt:
|
||||
return "Unashamed emotional expression. Heart icons from broken to sparkling, bold colors, intuitive selection."
|
||||
case .minimal:
|
||||
return "Only the essentials. Clean typography, flat design, and zero distractions."
|
||||
case .luxe:
|
||||
return "Liquid glass and premium materials. Cutting-edge iOS design language for the discerning user."
|
||||
case .forecast:
|
||||
return "Your mood is the weather. Storm to sunshine icons, flowing wave entries, and natural intuition."
|
||||
case .playful:
|
||||
return "Life's too short to be serious. Vibrant neons, familiar emoji, and game-like interaction."
|
||||
case .journal:
|
||||
return "Like writing in a physical diary. Stacked paper notes, handwritten feel, and intimate reflection."
|
||||
}
|
||||
}
|
||||
|
||||
var emoji: String {
|
||||
switch self {
|
||||
case .zenGarden: return "🧘"
|
||||
case .synthwave: return "🌆"
|
||||
case .celestial: return "✨"
|
||||
case .editorial: return "📰"
|
||||
case .mixtape: return "📼"
|
||||
case .bloom: return "🌸"
|
||||
case .heartfelt: return "💖"
|
||||
case .minimal: return "◽"
|
||||
case .luxe: return "💎"
|
||||
case .forecast: return "🌦️"
|
||||
case .playful: return "🎮"
|
||||
case .journal: return "📒"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Theme Components
|
||||
|
||||
var colorTint: MoodTints {
|
||||
switch self {
|
||||
case .zenGarden: return .Pastel
|
||||
case .synthwave: return .Neon
|
||||
case .celestial: return .Default
|
||||
case .editorial: return .Pastel
|
||||
case .mixtape: return .Default
|
||||
case .bloom: return .Pastel
|
||||
case .heartfelt: return .Pastel
|
||||
case .minimal: return .Pastel
|
||||
case .luxe: return .Default
|
||||
case .forecast: return .Default
|
||||
case .playful: return .Neon
|
||||
case .journal: return .Default
|
||||
}
|
||||
}
|
||||
|
||||
var iconPack: MoodImages {
|
||||
switch self {
|
||||
case .zenGarden: return .Garden
|
||||
case .synthwave: return .Cosmic
|
||||
case .celestial: return .Cosmic
|
||||
case .editorial: return .FontAwesome
|
||||
case .mixtape: return .Emoji
|
||||
case .bloom: return .Garden
|
||||
case .heartfelt: return .Hearts
|
||||
case .minimal: return .FontAwesome
|
||||
case .luxe: return .Cosmic
|
||||
case .forecast: return .Weather
|
||||
case .playful: return .Emoji
|
||||
case .journal: return .FontAwesome
|
||||
}
|
||||
}
|
||||
|
||||
var entryStyle: DayViewStyle {
|
||||
switch self {
|
||||
case .zenGarden: return .ink
|
||||
case .synthwave: return .neon
|
||||
case .celestial: return .orbit
|
||||
case .editorial: return .chronicle
|
||||
case .mixtape: return .tape
|
||||
case .bloom: return .morph
|
||||
case .heartfelt: return .bubble
|
||||
case .minimal: return .minimal
|
||||
case .luxe: return .glass
|
||||
case .forecast: return .wave
|
||||
case .playful: return .pattern
|
||||
case .journal: return .stack
|
||||
}
|
||||
}
|
||||
|
||||
var votingLayout: VotingLayoutStyle {
|
||||
switch self {
|
||||
case .zenGarden: return .stacked
|
||||
case .synthwave: return .neon
|
||||
case .celestial: return .orbit
|
||||
case .editorial: return .horizontal
|
||||
case .mixtape: return .cards
|
||||
case .bloom: return .aura
|
||||
case .heartfelt: return .radial
|
||||
case .minimal: return .horizontal
|
||||
case .luxe: return .aura
|
||||
case .forecast: return .radial
|
||||
case .playful: return .cards
|
||||
case .journal: return .stacked
|
||||
}
|
||||
}
|
||||
|
||||
var paywallStyle: PaywallStyle {
|
||||
switch self {
|
||||
case .zenGarden: return .zen
|
||||
case .synthwave: return .neon
|
||||
case .celestial: return .celestial
|
||||
case .editorial: return .editorial
|
||||
case .mixtape: return .mixtape
|
||||
case .bloom: return .garden
|
||||
case .heartfelt: return .heartfelt
|
||||
case .minimal: return .minimal
|
||||
case .luxe: return .luxe
|
||||
case .forecast: return .forecast
|
||||
case .playful: return .playful
|
||||
case .journal: return .journal
|
||||
}
|
||||
}
|
||||
|
||||
var lockScreenStyle: LockScreenStyle {
|
||||
switch self {
|
||||
case .zenGarden: return .zen
|
||||
case .synthwave: 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
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview Colors (for theme picker UI)
|
||||
|
||||
var previewColors: [Color] {
|
||||
switch self {
|
||||
case .zenGarden:
|
||||
return [Color(hex: "#C1E1C1"), Color(hex: "#A7C7E7"), Color(hex: "#fdfd96")]
|
||||
case .synthwave:
|
||||
return [Color(red: 0, green: 1, blue: 0.82), Color(red: 1, green: 0, blue: 0.8), Color(hex: "#050510")]
|
||||
case .celestial:
|
||||
return [Color(hex: "#0b84ff"), Color(hex: "#31d158"), Color(hex: "#1a1a2e")]
|
||||
case .editorial:
|
||||
return [Color(hex: "#A7C7E7"), Color(hex: "#2c2c2c"), Color(hex: "#f5f5f5")]
|
||||
case .mixtape:
|
||||
return [Color(hex: "#8B4513"), Color(hex: "#D2691E"), Color(hex: "#2c2c2c")]
|
||||
case .bloom:
|
||||
return [Color(hex: "#C1E1C1"), Color(hex: "#ffb347"), Color(hex: "#FF6961")]
|
||||
case .heartfelt:
|
||||
return [Color(hex: "#FF6961"), Color(hex: "#ffb347"), Color(hex: "#C1E1C1")]
|
||||
case .minimal:
|
||||
return [Color(hex: "#A7C7E7"), Color(hex: "#e0e0e0"), Color(hex: "#f8f8f8")]
|
||||
case .luxe:
|
||||
return [Color(hex: "#0b84ff"), Color(hex: "#31d158"), Color.white.opacity(0.8)]
|
||||
case .forecast:
|
||||
return [Color(hex: "#0b84ff"), Color(hex: "#ffd709"), Color(hex: "#ff453a")]
|
||||
case .playful:
|
||||
return [Color(hex: "#39FF14"), Color(hex: "#FFF01F"), Color(hex: "#FF5F1F")]
|
||||
case .journal:
|
||||
return [Color(hex: "#D2B48C"), Color(hex: "#8B4513"), Color(hex: "#FFFEF0")]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Apply Theme
|
||||
|
||||
/// Applies all theme settings to UserDefaults
|
||||
func apply() {
|
||||
// Set color tint
|
||||
GroupUserDefaults.groupDefaults.set(colorTint.rawValue, forKey: UserDefaultsStore.Keys.moodTint.rawValue)
|
||||
|
||||
// Set icon pack
|
||||
GroupUserDefaults.groupDefaults.set(iconPack.rawValue, forKey: UserDefaultsStore.Keys.moodImages.rawValue)
|
||||
|
||||
// Set entry style
|
||||
GroupUserDefaults.groupDefaults.set(entryStyle.rawValue, forKey: UserDefaultsStore.Keys.dayViewStyle.rawValue)
|
||||
|
||||
// Set voting layout
|
||||
GroupUserDefaults.groupDefaults.set(votingLayout.rawValue, forKey: UserDefaultsStore.Keys.votingLayoutStyle.rawValue)
|
||||
|
||||
// Set paywall style
|
||||
GroupUserDefaults.groupDefaults.set(paywallStyle.rawValue, forKey: UserDefaultsStore.Keys.paywallStyle.rawValue)
|
||||
|
||||
// Set lock screen style
|
||||
GroupUserDefaults.groupDefaults.set(lockScreenStyle.rawValue, forKey: UserDefaultsStore.Keys.lockScreenStyle.rawValue)
|
||||
|
||||
// Log the theme change
|
||||
EventLogger.log(event: "apply_theme", withData: ["theme": name])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Theme Categories for Browsing
|
||||
|
||||
extension AppTheme {
|
||||
enum Category: String, CaseIterable {
|
||||
case calm = "Calm & Mindful"
|
||||
case energetic = "Energetic & Bold"
|
||||
case sophisticated = "Sophisticated"
|
||||
case expressive = "Expressive"
|
||||
|
||||
var themes: [AppTheme] {
|
||||
switch self {
|
||||
case .calm:
|
||||
return [.zenGarden, .minimal, .bloom]
|
||||
case .energetic:
|
||||
return [.synthwave, .playful, .mixtape]
|
||||
case .sophisticated:
|
||||
return [.celestial, .editorial, .luxe]
|
||||
case .expressive:
|
||||
return [.heartfelt, .forecast, .journal]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,14 @@ enum PaywallStyle: Int, CaseIterable {
|
||||
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 {
|
||||
@@ -41,6 +49,14 @@ enum PaywallStyle: Int, CaseIterable {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +66,48 @@ enum PaywallStyle: Int, CaseIterable {
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,6 +191,7 @@ class UserDefaultsStore {
|
||||
case healthKitEnabled
|
||||
case healthKitSyncEnabled
|
||||
case paywallStyle
|
||||
case lockScreenStyle
|
||||
|
||||
case contentViewCurrentSelectedHeaderViewBackDays
|
||||
case contentViewHeaderTag
|
||||
|
||||
Reference in New Issue
Block a user