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:
Trey t
2025-12-26 23:53:35 -06:00
parent 53eb953b77
commit a0b30d8bae
8 changed files with 4084 additions and 77 deletions

View 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]
}
}
}
}

View File

@@ -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