diff --git a/Feels/Localizable.xcstrings b/Feels/Localizable.xcstrings index f355547..ca80baf 100644 --- a/Feels/Localizable.xcstrings +++ b/Feels/Localizable.xcstrings @@ -558,8 +558,37 @@ "comment" : "A symbol that appears before a command in a terminal interface.", "isCommentAutoGenerated" : true }, + "โœจ" : { + + }, + "๐ŸŒˆ" : { + + }, + "๐ŸŽ‰" : { + "comment" : "A smiling face emoji.", + "isCommentAutoGenerated" : true + }, + "๐ŸŽจ" : { + "comment" : "An emoji used as a preview for a theme in the CustomizeContentView.", + "isCommentAutoGenerated" : true + }, + "๐Ÿ’ซ" : { + + }, + "๐Ÿ˜„" : { + "comment" : "A smiling face emoji.", + "isCommentAutoGenerated" : true + }, + "๐Ÿ˜Š" : { + "comment" : "A playful emoji used in the lock screen view.", + "isCommentAutoGenerated" : true + }, "12" : { + }, + "12 curated combinations of colors, icons, and layouts" : { + "comment" : "A description of the 12 color, icon, and layout combinations available in the theme picker.", + "isCommentAutoGenerated" : true }, "17" : { @@ -1190,8 +1219,8 @@ "comment" : "An accessibility label for the section of the settings view that indicates that Apple Health is not available on the user's device.", "isCommentAutoGenerated" : true }, - "are safe here" : { - "comment" : "A description of the safety of the app's \"Aurora background\".", + "Apply %@ Theme" : { + "comment" : "A button that, when tapped, applies a theme to the app. The label inside the button changes to reflect the name of the theme being applied.", "isCommentAutoGenerated" : true }, "Are you sure you want to delete this mood entry? This cannot be undone." : { @@ -1321,10 +1350,6 @@ } } }, - "Authenticate to continue your journey" : { - "comment" : "A description of the action required to unlock the app.", - "isCommentAutoGenerated" : true - }, "Authentication Failed" : { "comment" : "An alert title when authentication fails.", "isCommentAutoGenerated" : true, @@ -1367,6 +1392,10 @@ } } }, + "Browse Themes" : { + "comment" : "A suggestion to the user to explore different color and layout combinations.", + "isCommentAutoGenerated" : true + }, "Build Your Streak!" : { "comment" : "A title for a tip that encourages users to build a mood streak.", "isCommentAutoGenerated" : true, @@ -1787,6 +1816,10 @@ } } }, + "Choose Your Vibe" : { + "comment" : "A title displayed at the top of the view, encouraging the user to choose a theme.", + "isCommentAutoGenerated" : true + }, "Clarity through simplicity.\nPremium unlocks understanding." : { "comment" : "A description of the benefits of the premium subscription.", "isCommentAutoGenerated" : true @@ -4506,6 +4539,10 @@ "comment" : "A hint that appears when tapping on an icon to select it.", "isCommentAutoGenerated" : true }, + "Each theme combines colors, icons, layouts, and styles into a cohesive experience." : { + "comment" : "A description of how themes work.", + "isCommentAutoGenerated" : true + }, "Edit" : { "comment" : "A button label that triggers the editing of a journal note.", "isCommentAutoGenerated" : true, @@ -4548,6 +4585,13 @@ } } }, + "Elevate Your\nEmotional Life" : { + + }, + "ELEVATE YOUR\nEXPERIENCE" : { + "comment" : "A title for the section that describes the premium theme.", + "isCommentAutoGenerated" : true + }, "Entries" : { "comment" : "A label describing the number of mood entries.", "isCommentAutoGenerated" : true, @@ -4674,10 +4718,18 @@ } } }, + "Every entry tells your story.\nPremium unlocks the full narrative." : { + "comment" : "A description of the premium feature, emphasizing the narrative aspect of journaling.", + "isCommentAutoGenerated" : true + }, "Every feeling is a seed.\nPremium helps you grow." : { "comment" : "A description of the premium subscription's benefits.", "isCommentAutoGenerated" : true }, + "Every feeling is a track.\nPremium gives you the full album." : { + "comment" : "A description of the premium subscription that highlights the unlimited access to all tracks.", + "isCommentAutoGenerated" : true + }, "Exit" : { "comment" : "A button label that dismisses the current view.", "isCommentAutoGenerated" : true, @@ -5025,6 +5077,13 @@ } } } + }, + "Feel It All" : { + "comment" : "The title of a section that highlights the premium feature of the app.", + "isCommentAutoGenerated" : true + }, + "Feel With\nAll Your Heart" : { + }, "Feels" : { "comment" : "The name of the widget.", @@ -5068,6 +5127,10 @@ } } }, + "FEELS" : { + "comment" : "The name of the app.", + "isCommentAutoGenerated" : true + }, "Feels Icon" : { "comment" : "Name of the widget configuration.", "isCommentAutoGenerated" : true, @@ -5153,6 +5216,10 @@ } } }, + "FEELS MIXTAPE" : { + "comment" : "The name of the premium subscription experience.", + "isCommentAutoGenerated" : true + }, "filter_view_total" : { "extractionState" : "manual", "localizations" : { @@ -5199,6 +5266,13 @@ } } } + }, + "Find Your\nInner Calm" : { + "comment" : "A title describing the main benefit of the premium subscription.", + "isCommentAutoGenerated" : true + }, + "Find Your\nInner Peace" : { + }, "Fix Weekday" : { "comment" : "A button label that, when tapped, will attempt to correct the user's calendar entries that are set to the wrong weekday.", @@ -5933,6 +6007,10 @@ "comment" : "A description of the social proof badge.", "isCommentAutoGenerated" : true }, + "JOURNAL" : { + "comment" : "The title of the journal.", + "isCommentAutoGenerated" : true + }, "Journal Note" : { "comment" : "The title of the view that appears in the navigation bar.", "isCommentAutoGenerated" : true, @@ -6099,6 +6177,14 @@ } } }, + "Level up your self-awareness.\nPremium = MAX POWER!" : { + "comment" : "A tagline describing the premium subscription and its benefits.", + "isCommentAutoGenerated" : true + }, + "Like ink on paper, each mood\nleaves its mark. Premium reveals the pattern." : { + "comment" : "A description of how the premium version reveals patterns in user moods.", + "isCommentAutoGenerated" : true + }, "Log Mood" : { "comment" : "A button that opens Feels to log a mood.", "isCommentAutoGenerated" : true, @@ -6349,6 +6435,9 @@ } } } + }, + "Make Tracking\nFun Again!" : { + }, "Manage" : { "comment" : "A button that allows the user to manage their subscription.", @@ -6736,6 +6825,10 @@ } } }, + "MOOD UNLOCKED! ๐ŸŽฎ" : { + "comment" : "A catchy title for the premium subscription content.", + "isCommentAutoGenerated" : true + }, "Mood Vote" : { "comment" : "The display name of the widget configuration.", "isCommentAutoGenerated" : true, @@ -8933,6 +9026,10 @@ } } }, + "Predict your patterns.\nPrepare for any weather." : { + "comment" : "A description of the premium feature, \"Your Emotional Forecast\", that appears below the title.", + "isCommentAutoGenerated" : true + }, "Premium Active" : { "localizations" : { "de" : { @@ -8973,6 +9070,10 @@ } } }, + "Premium Edition" : { + "comment" : "A title for the premium edition of the app.", + "isCommentAutoGenerated" : true + }, "Premium Feature" : { "comment" : "A label indicating a premium feature.", "isCommentAutoGenerated" : true, @@ -9019,6 +9120,10 @@ "comment" : "A description of a premium feature that requires a subscription.", "isCommentAutoGenerated" : true }, + "Premium refinement for those\nwho expect the finest." : { + "comment" : "A description of the premium theme, highlighting its exclusivity and superior quality.", + "isCommentAutoGenerated" : true + }, "Preview" : { }, @@ -12538,6 +12643,24 @@ } } } + }, + "THE" : { + "comment" : "The first word of the title of the magazine.", + "isCommentAutoGenerated" : true + }, + "THE ART\nOF FEELING" : { + + }, + "Themes" : { + "comment" : "The title of the view where they can choose different themes.", + "isCommentAutoGenerated" : true + }, + "Themes set all four options at once" : { + "comment" : "An explanatory note about how themes affect all four settings.", + "isCommentAutoGenerated" : true + }, + "This theme includes" : { + }, "Tip: \"Yesterday\" works great for evening reminders" : { "comment" : "A tip displayed below the \"Today\" and \"Yesterday\" options in the onboarding flow.", @@ -13217,6 +13340,10 @@ } } }, + "Unlock the complete story\nof your emotional landscape." : { + "comment" : "A description of the premium edition of the app.", + "isCommentAutoGenerated" : true + }, "Unlock the Full\nExperience" : { "comment" : "A title displayed in the onboarding view, promoting the full experience that can be unlocked with a subscription.", "isCommentAutoGenerated" : true, @@ -13995,6 +14122,9 @@ } } } + }, + "Write Your\nEmotional Story" : { + }, "Year in Review" : { "comment" : "A description below the year's mood breakdown.", @@ -14080,6 +14210,10 @@ } } }, + "You can still customize individual settings after applying a theme" : { + "comment" : "A note explaining that users can still customize individual settings after applying a theme.", + "isCommentAutoGenerated" : true + }, "You don't have a streak yet. Log your mood today to start one!" : { "comment" : "Text displayed in a notification when a user has not logged a mood for the current day.", "isCommentAutoGenerated" : true, @@ -14290,12 +14424,24 @@ } } }, + "Your Emotional\nForecast" : { + "comment" : "The title of the section that describes the main feature of the premium subscription.", + "isCommentAutoGenerated" : true + }, "Your emotions tell a story.\nPremium helps you read it." : { "comment" : "A subheadline describing the benefits of the premium subscription.", "isCommentAutoGenerated" : true }, - "Your Feelings" : { - "comment" : "The title of the main screen in the lock screen.", + "Your heart knows the way.\nPremium helps you listen." : { + "comment" : "A description of the premium subscription service, emphasizing its ability to help users listen to their hearts.", + "isCommentAutoGenerated" : true + }, + "YOUR MOOD\nMIXTAPE" : { + "comment" : "The title of the mixtape theme's main content.", + "isCommentAutoGenerated" : true + }, + "Your Personal\nDiary" : { + "comment" : "The title of the premium subscription feature.", "isCommentAutoGenerated" : true } }, diff --git a/Shared/Models/AppTheme.swift b/Shared/Models/AppTheme.swift new file mode 100644 index 0000000..75ffe85 --- /dev/null +++ b/Shared/Models/AppTheme.swift @@ -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] + } + } + } +} diff --git a/Shared/Models/UserDefaultsStore.swift b/Shared/Models/UserDefaultsStore.swift index 5f5f6ba..02eeee8 100644 --- a/Shared/Models/UserDefaultsStore.swift +++ b/Shared/Models/UserDefaultsStore.swift @@ -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 diff --git a/Shared/Views/CustomizeView/CustomizeView.swift b/Shared/Views/CustomizeView/CustomizeView.swift index 0cc352a..5dc7b6d 100644 --- a/Shared/Views/CustomizeView/CustomizeView.swift +++ b/Shared/Views/CustomizeView/CustomizeView.swift @@ -15,6 +15,8 @@ struct CustomizeContentView: View { @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system + @State private var showThemePicker = false + var body: some View { ScrollView { VStack(spacing: 24) { @@ -22,6 +24,50 @@ struct CustomizeContentView: View { TipView(CustomizeLayoutTip()) .tipBackground(Color(.secondarySystemBackground)) + // QUICK THEMES + SettingsSection(title: "Quick Start") { + Button(action: { showThemePicker = true }) { + HStack(spacing: 16) { + // Emoji preview + ZStack { + LinearGradient( + colors: [.purple.opacity(0.8), .blue.opacity(0.8), .cyan.opacity(0.8)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + Text("๐ŸŽจ") + .font(.title) + } + .frame(width: 56, height: 56) + .clipShape(RoundedRectangle(cornerRadius: 12)) + + VStack(alignment: .leading, spacing: 4) { + Text("Browse Themes") + .font(.headline) + .foregroundColor(.primary) + + Text("12 curated combinations of colors, icons, and layouts") + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(2) + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.subheadline.weight(.semibold)) + .foregroundColor(.secondary) + } + .padding(12) + .background(colorScheme == .dark ? Color(.systemGray6) : .white) + .clipShape(RoundedRectangle(cornerRadius: 16)) + } + .buttonStyle(.plain) + } + .sheet(isPresented: $showThemePicker) { + AppThemePickerView() + } + // APPEARANCE SettingsSection(title: "Appearance") { VStack(spacing: 16) { diff --git a/Shared/Views/CustomizeView/SubViews/AppThemePickerView.swift b/Shared/Views/CustomizeView/SubViews/AppThemePickerView.swift new file mode 100644 index 0000000..072dc87 --- /dev/null +++ b/Shared/Views/CustomizeView/SubViews/AppThemePickerView.swift @@ -0,0 +1,414 @@ +// +// AppThemePickerView.swift +// Feels (iOS) +// +// Created by Claude Code on 12/26/24. +// + +import SwiftUI + +struct AppThemePickerView: View { + @Environment(\.dismiss) private var dismiss + @Environment(\.colorScheme) private var colorScheme + + @AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) + private var moodTint: Int = 0 + + @AppStorage(UserDefaultsStore.Keys.moodImages.rawValue, store: GroupUserDefaults.groupDefaults) + private var moodImages: Int = 0 + + @AppStorage(UserDefaultsStore.Keys.dayViewStyle.rawValue, store: GroupUserDefaults.groupDefaults) + private var dayViewStyle: Int = 0 + + @AppStorage(UserDefaultsStore.Keys.votingLayoutStyle.rawValue, store: GroupUserDefaults.groupDefaults) + private var votingLayoutStyle: Int = 0 + + @State private var selectedTheme: AppTheme? + + var body: some View { + NavigationStack { + ScrollView { + VStack(spacing: 32) { + // Header + headerSection + + // Theme Grid + LazyVGrid(columns: [ + GridItem(.flexible(), spacing: 16), + GridItem(.flexible(), spacing: 16) + ], spacing: 20) { + ForEach(AppTheme.allCases) { theme in + AppThemeCard( + theme: theme, + isSelected: isThemeActive(theme), + onTap: { selectTheme(theme) } + ) + } + } + .padding(.horizontal, 20) + + // Footer note + footerNote + .padding(.bottom, 40) + } + } + .background(colorScheme == .dark ? Color.black : Color(.systemGroupedBackground)) + .navigationTitle("Themes") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("Done") { + dismiss() + } + } + } + .sheet(item: $selectedTheme) { theme in + AppThemePreviewSheet(theme: theme) { + applyTheme(theme) + } + } + } + } + + // MARK: - Subviews + + private var headerSection: some View { + VStack(spacing: 12) { + Text("Choose Your Vibe") + .font(.system(.title, design: .rounded, weight: .bold)) + .foregroundColor(.primary) + + Text("Each theme combines colors, icons, layouts, and styles into a cohesive experience.") + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + } + .padding(.top, 20) + } + + private var footerNote: some View { + VStack(spacing: 8) { + Text("Themes set all four options at once") + .font(.caption) + .foregroundColor(.secondary) + + Text("You can still customize individual settings after applying a theme") + .font(.caption2) + .foregroundColor(.secondary.opacity(0.7)) + } + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + } + + // MARK: - Logic + + private func isThemeActive(_ theme: AppTheme) -> Bool { + return moodTint == theme.colorTint.rawValue && + moodImages == theme.iconPack.rawValue && + dayViewStyle == theme.entryStyle.rawValue && + votingLayoutStyle == theme.votingLayout.rawValue + } + + private func selectTheme(_ theme: AppTheme) { + selectedTheme = theme + } + + private func applyTheme(_ theme: AppTheme) { + withAnimation(.easeInOut(duration: 0.3)) { + theme.apply() + selectedTheme = nil + } + } +} + +// MARK: - Theme Card + +struct AppThemeCard: View { + let theme: AppTheme + let isSelected: Bool + let onTap: () -> Void + + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + Button(action: onTap) { + VStack(spacing: 0) { + // Preview area + ZStack { + // Background gradient + LinearGradient( + colors: theme.previewColors, + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + + // Theme emoji + Text(theme.emoji) + .font(.system(size: 44)) + .shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 2) + + // Selected checkmark + if isSelected { + VStack { + HStack { + Spacer() + Image(systemName: "checkmark.circle.fill") + .font(.title2) + .foregroundStyle(.white) + .background( + Circle() + .fill(.green) + .frame(width: 28, height: 28) + ) + .padding(8) + } + Spacer() + } + } + } + .frame(height: 100) + .clipShape( + UnevenRoundedRectangle( + topLeadingRadius: 16, + bottomLeadingRadius: 0, + bottomTrailingRadius: 0, + topTrailingRadius: 16 + ) + ) + + // Info area + VStack(alignment: .leading, spacing: 4) { + Text(theme.name) + .font(.headline) + .foregroundColor(.primary) + .lineLimit(1) + + Text(theme.tagline) + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(1) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 12) + .padding(.vertical, 10) + .background(colorScheme == .dark ? Color(.systemGray6) : .white) + .clipShape( + UnevenRoundedRectangle( + topLeadingRadius: 0, + bottomLeadingRadius: 16, + bottomTrailingRadius: 16, + topTrailingRadius: 0 + ) + ) + } + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(isSelected ? Color.accentColor : Color.clear, lineWidth: 3) + ) + .shadow( + color: colorScheme == .dark ? .clear : .black.opacity(0.08), + radius: 8, + x: 0, + y: 4 + ) + } + .buttonStyle(.plain) + } +} + +// MARK: - Theme Preview Sheet + +struct AppThemePreviewSheet: View { + let theme: AppTheme + let onApply: () -> Void + + @Environment(\.dismiss) private var dismiss + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + NavigationStack { + ScrollView { + VStack(spacing: 24) { + // Hero + heroSection + + // What's included + componentsSection + + // Apply button + applyButton + .padding(.bottom, 40) + } + } + .background(colorScheme == .dark ? Color.black : Color(.systemGroupedBackground)) + .navigationTitle(theme.name) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Cancel") { + dismiss() + } + } + } + } + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.visible) + } + + private var heroSection: some View { + ZStack { + // Gradient background + LinearGradient( + colors: theme.previewColors + [theme.previewColors[0].opacity(0.5)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + + VStack(spacing: 16) { + Text(theme.emoji) + .font(.system(size: 72)) + .shadow(color: .black.opacity(0.3), radius: 8, x: 0, y: 4) + + Text(theme.tagline) + .font(.title3.weight(.medium)) + .foregroundColor(.white) + .shadow(color: .black.opacity(0.3), radius: 4, x: 0, y: 2) + } + } + .frame(height: 200) + .clipShape(RoundedRectangle(cornerRadius: 20)) + .padding(.horizontal, 20) + .padding(.top, 16) + } + + private var componentsSection: some View { + VStack(alignment: .leading, spacing: 16) { + Text("This theme includes") + .font(.headline) + .padding(.horizontal, 20) + + VStack(spacing: 12) { + ThemeComponentRow( + icon: "paintpalette.fill", + title: "Colors", + value: theme.colorTint == .Default ? "Default" : + theme.colorTint == .Neon ? "Neon" : + theme.colorTint == .Pastel ? "Pastel" : "Custom", + color: .orange + ) + + ThemeComponentRow( + icon: "face.smiling.fill", + title: "Icons", + value: iconName(for: theme.iconPack), + color: .purple + ) + + ThemeComponentRow( + icon: "rectangle.stack.fill", + title: "Entry Style", + value: theme.entryStyle.displayName, + color: .blue + ) + + ThemeComponentRow( + icon: "hand.tap.fill", + title: "Voting Layout", + value: theme.votingLayout.displayName, + color: .green + ) + } + .padding(.horizontal, 20) + + // Description + Text(theme.description) + .font(.subheadline) + .foregroundColor(.secondary) + .padding(.horizontal, 20) + .padding(.top, 8) + } + } + + private var applyButton: some View { + Button(action: { + onApply() + dismiss() + }) { + HStack { + Image(systemName: "paintbrush.fill") + Text("Apply \(theme.name) Theme") + } + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .background( + LinearGradient( + colors: [theme.previewColors[0], theme.previewColors[1]], + startPoint: .leading, + endPoint: .trailing + ) + ) + .clipShape(RoundedRectangle(cornerRadius: 14)) + .shadow(color: theme.previewColors[0].opacity(0.4), radius: 8, x: 0, y: 4) + } + .padding(.horizontal, 20) + } + + private func iconName(for pack: MoodImages) -> String { + switch pack { + case .FontAwesome: return "Classic Faces" + case .Emoji: return "Emoji" + case .HandEmjoi: return "Hand Gestures" + case .Weather: return "Weather" + case .Garden: return "Garden" + case .Hearts: return "Hearts" + case .Cosmic: return "Cosmic" + } + } +} + +// MARK: - Component Row + +struct ThemeComponentRow: View { + let icon: String + let title: String + let value: String + let color: Color + + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + HStack(spacing: 14) { + Image(systemName: icon) + .font(.title3) + .foregroundColor(color) + .frame(width: 36, height: 36) + .background(color.opacity(0.15)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + + Text(title) + .font(.subheadline) + .foregroundColor(.secondary) + + Spacer() + + Text(value) + .font(.subheadline.weight(.medium)) + .foregroundColor(.primary) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background(colorScheme == .dark ? Color(.systemGray6) : .white) + .clipShape(RoundedRectangle(cornerRadius: 12)) + } +} + +// MARK: - Preview + +struct AppThemePickerView_Previews: PreviewProvider { + static var previews: some View { + AppThemePickerView() + } +} diff --git a/Shared/Views/FeelsSubscriptionStoreView.swift b/Shared/Views/FeelsSubscriptionStoreView.swift index 6420da6..80fca65 100644 --- a/Shared/Views/FeelsSubscriptionStoreView.swift +++ b/Shared/Views/FeelsSubscriptionStoreView.swift @@ -11,7 +11,19 @@ import StoreKit struct FeelsSubscriptionStoreView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject var iapManager: IAPManager - var style: PaywallStyle = .celestial + + // Read from AppStorage to match current theme, with optional override for previews + @AppStorage(UserDefaultsStore.Keys.paywallStyle.rawValue, store: GroupUserDefaults.groupDefaults) + private var paywallStyleRaw: Int = 0 + + var style: PaywallStyle? + + private var currentStyle: PaywallStyle { + if let override = style { + return override + } + return PaywallStyle(rawValue: paywallStyleRaw) ?? .celestial + } var body: some View { SubscriptionStoreView(groupID: IAPManager.subscriptionGroupID) { @@ -30,7 +42,7 @@ struct FeelsSubscriptionStoreView: View { @ViewBuilder private var marketingContent: some View { - switch style { + switch currentStyle { case .celestial: CelestialMarketingContent() case .garden: @@ -39,15 +51,39 @@ struct FeelsSubscriptionStoreView: View { NeonMarketingContent() case .minimal: MinimalMarketingContent() + case .zen: + ZenMarketingContent() + case .editorial: + EditorialMarketingContent() + case .mixtape: + MixtapeMarketingContent() + case .heartfelt: + HeartfeltMarketingContent() + case .luxe: + LuxeMarketingContent() + case .forecast: + ForecastMarketingContent() + case .playful: + PlayfulMarketingContent() + case .journal: + JournalMarketingContent() } } private var tintColor: Color { - switch style { + switch currentStyle { case .celestial: return Color(red: 1.0, green: 0.4, blue: 0.5) case .garden: return Color(red: 0.4, green: 0.75, blue: 0.45) case .neon: return Color(red: 0.0, green: 1.0, blue: 0.8) case .minimal: return Color(red: 0.95, green: 0.6, blue: 0.5) + case .zen: return Color(red: 0.4, green: 0.6, blue: 0.5) + case .editorial: return Color(red: 0.2, green: 0.2, blue: 0.3) + case .mixtape: return Color(red: 0.8, green: 0.5, blue: 0.3) + case .heartfelt: return Color(red: 1.0, green: 0.4, blue: 0.5) + case .luxe: return Color(red: 0.6, green: 0.7, blue: 0.9) + case .forecast: return Color(red: 0.3, green: 0.6, blue: 0.9) + case .playful: return Color(red: 0.2, green: 1.0, blue: 0.4) + case .journal: return Color(red: 0.6, green: 0.5, blue: 0.4) } } } @@ -325,6 +361,506 @@ struct MinimalMarketingContent: View { } } +// MARK: - 5. Zen Theme (Ink Brushstrokes & Meditation) + +struct ZenMarketingContent: View { + @State private var showContent = false + @State private var inkFlow = false + + var body: some View { + ZStack { + ZenBackground(inkFlow: $inkFlow) + + VStack(spacing: 0) { + // Zen circle (ensล) + ZenEnsoView(animate: $inkFlow) + .frame(height: 160) + .padding(.top, 10) + + VStack(spacing: 20) { + Text("Find Your\nInner Calm") + .font(.system(size: 34, weight: .light, design: .serif)) + .italic() + .multilineTextAlignment(.center) + .foregroundColor(Color(red: 0.95, green: 0.93, blue: 0.88)) + .shadow(color: .black.opacity(0.3), radius: 8) + + Text("Like ink on paper, each mood\nleaves its mark. Premium reveals the pattern.") + .font(.system(size: 15, weight: .regular, design: .serif)) + .multilineTextAlignment(.center) + .foregroundColor(Color(red: 0.8, green: 0.75, blue: 0.7)) + .lineSpacing(4) + } + .padding(.horizontal, 32) + .opacity(showContent ? 1 : 0) + .offset(y: showContent ? 0 : 20) + + FeatureCardsGrid(style: .zen) + .padding(.top, 28) + .padding(.horizontal, 28) + .opacity(showContent ? 1 : 0) + + SocialProofBadge(style: .zen) + .padding(.top, 24) + .opacity(showContent ? 1 : 0) + + Spacer().frame(height: 20) + } + } + .onAppear { + withAnimation(.easeInOut(duration: 4).repeatForever(autoreverses: true)) { + inkFlow = true + } + withAnimation(.easeOut(duration: 1.0).delay(0.3)) { + showContent = true + } + } + } +} + +// MARK: - 6. Editorial Theme (Magazine Typography) + +struct EditorialMarketingContent: View { + @State private var showContent = false + + var body: some View { + ZStack { + EditorialBackground() + + VStack(spacing: 0) { + // Large typographic element + VStack(spacing: 4) { + Text("THE") + .font(.system(size: 14, weight: .regular, design: .serif)) + .tracking(8) + .foregroundColor(Color(red: 0.4, green: 0.4, blue: 0.45)) + + Text("FEELS") + .font(.system(size: 52, weight: .bold, design: .serif)) + .tracking(2) + .foregroundColor(Color(red: 0.15, green: 0.15, blue: 0.2)) + + Text("JOURNAL") + .font(.system(size: 14, weight: .light, design: .serif)) + .tracking(12) + .foregroundColor(Color(red: 0.4, green: 0.4, blue: 0.45)) + } + .padding(.top, 30) + .padding(.bottom, 20) + + VStack(spacing: 16) { + Rectangle() + .fill(Color(red: 0.2, green: 0.2, blue: 0.25)) + .frame(width: 60, height: 1) + + Text("Premium Edition") + .font(.system(size: 13, weight: .medium, design: .serif)) + .italic() + .foregroundColor(Color(red: 0.5, green: 0.5, blue: 0.55)) + + Text("Unlock the complete story\nof your emotional landscape.") + .font(.system(size: 16, weight: .regular, design: .serif)) + .multilineTextAlignment(.center) + .foregroundColor(Color(red: 0.3, green: 0.3, blue: 0.35)) + .lineSpacing(6) + } + .padding(.horizontal, 40) + .opacity(showContent ? 1 : 0) + + FeatureCardsGrid(style: .editorial) + .padding(.top, 28) + .padding(.horizontal, 28) + .opacity(showContent ? 1 : 0) + + SocialProofBadge(style: .editorial) + .padding(.top, 24) + .opacity(showContent ? 1 : 0) + + Spacer().frame(height: 20) + } + } + .onAppear { + withAnimation(.easeOut(duration: 0.8).delay(0.2)) { + showContent = true + } + } + } +} + +// MARK: - 7. Mixtape Theme (Cassette & Retro Analog) + +struct MixtapeMarketingContent: View { + @State private var showContent = false + @State private var tapeRotation = false + + var body: some View { + ZStack { + MixtapeBackground() + + VStack(spacing: 0) { + // Cassette tape illustration + CassetteTapeView(rotating: $tapeRotation) + .frame(height: 150) + .padding(.top, 15) + + VStack(spacing: 16) { + Text("YOUR MOOD\nMIXTAPE") + .font(.system(size: 30, weight: .bold, design: .rounded)) + .multilineTextAlignment(.center) + .foregroundStyle( + LinearGradient( + colors: [ + Color(red: 1.0, green: 0.85, blue: 0.6), + Color(red: 0.9, green: 0.6, blue: 0.4) + ], + startPoint: .top, + endPoint: .bottom + ) + ) + + Text("Every feeling is a track.\nPremium gives you the full album.") + .font(.system(size: 15, weight: .medium, design: .rounded)) + .multilineTextAlignment(.center) + .foregroundColor(Color(red: 0.85, green: 0.75, blue: 0.65)) + .lineSpacing(4) + } + .padding(.horizontal, 32) + .opacity(showContent ? 1 : 0) + .offset(y: showContent ? 0 : 20) + + FeatureCardsGrid(style: .mixtape) + .padding(.top, 28) + .padding(.horizontal, 28) + .opacity(showContent ? 1 : 0) + + SocialProofBadge(style: .mixtape) + .padding(.top, 24) + .opacity(showContent ? 1 : 0) + + Spacer().frame(height: 20) + } + } + .onAppear { + withAnimation(.linear(duration: 3).repeatForever(autoreverses: false)) { + tapeRotation = true + } + withAnimation(.easeOut(duration: 0.8).delay(0.2)) { + showContent = true + } + } + } +} + +// MARK: - 8. Heartfelt Theme (Hearts & Emotion) + +struct HeartfeltMarketingContent: View { + @State private var showContent = false + @State private var heartbeat = false + + var body: some View { + ZStack { + HeartfeltBackground(beat: $heartbeat) + + VStack(spacing: 0) { + // Floating hearts + FloatingHeartsView(beat: $heartbeat) + .frame(height: 150) + .padding(.top, 15) + + VStack(spacing: 16) { + Text("Feel It All") + .font(.system(size: 38, weight: .bold, design: .rounded)) + .foregroundStyle( + LinearGradient( + colors: [ + Color(red: 1.0, green: 0.6, blue: 0.7), + Color(red: 1.0, green: 0.4, blue: 0.5) + ], + startPoint: .top, + endPoint: .bottom + ) + ) + .shadow(color: Color(red: 1.0, green: 0.4, blue: 0.5).opacity(0.3), radius: 15) + + Text("Your heart knows the way.\nPremium helps you listen.") + .font(.system(size: 16, weight: .medium, design: .rounded)) + .multilineTextAlignment(.center) + .foregroundColor(.white.opacity(0.8)) + .lineSpacing(4) + } + .padding(.horizontal, 32) + .opacity(showContent ? 1 : 0) + .offset(y: showContent ? 0 : 20) + + FeatureCardsGrid(style: .heartfelt) + .padding(.top, 28) + .padding(.horizontal, 28) + .opacity(showContent ? 1 : 0) + + SocialProofBadge(style: .heartfelt) + .padding(.top, 24) + .opacity(showContent ? 1 : 0) + + Spacer().frame(height: 20) + } + } + .onAppear { + withAnimation(.easeInOut(duration: 0.8).repeatForever(autoreverses: true)) { + heartbeat = true + } + withAnimation(.easeOut(duration: 0.8).delay(0.2)) { + showContent = true + } + } + } +} + +// MARK: - 9. Luxe Theme (Premium Glass Materials) + +struct LuxeMarketingContent: View { + @State private var showContent = false + @State private var shimmer = false + + var body: some View { + ZStack { + LuxeBackground(shimmer: $shimmer) + + VStack(spacing: 0) { + // Diamond/gem icon + LuxeGemView(shimmer: $shimmer) + .frame(height: 150) + .padding(.top, 15) + + VStack(spacing: 16) { + Text("ELEVATE YOUR\nEXPERIENCE") + .font(.system(size: 28, weight: .semibold, design: .rounded)) + .multilineTextAlignment(.center) + .foregroundStyle( + LinearGradient( + colors: [.white, Color(red: 0.85, green: 0.9, blue: 1.0)], + startPoint: .top, + endPoint: .bottom + ) + ) + .shadow(color: .white.opacity(0.3), radius: 10) + + Text("Premium refinement for those\nwho expect the finest.") + .font(.system(size: 15, weight: .regular, design: .rounded)) + .multilineTextAlignment(.center) + .foregroundColor(.white.opacity(0.7)) + .lineSpacing(4) + } + .padding(.horizontal, 32) + .opacity(showContent ? 1 : 0) + .offset(y: showContent ? 0 : 20) + + FeatureCardsGrid(style: .luxe) + .padding(.top, 28) + .padding(.horizontal, 28) + .opacity(showContent ? 1 : 0) + + SocialProofBadge(style: .luxe) + .padding(.top, 24) + .opacity(showContent ? 1 : 0) + + Spacer().frame(height: 20) + } + } + .onAppear { + withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) { + shimmer = true + } + withAnimation(.easeOut(duration: 0.8).delay(0.2)) { + showContent = true + } + } + } +} + +// MARK: - 10. Forecast Theme (Weather Metaphors) + +struct ForecastMarketingContent: View { + @State private var showContent = false + @State private var cloudDrift = false + + var body: some View { + ZStack { + ForecastBackground(drift: $cloudDrift) + + VStack(spacing: 0) { + // Weather icons + WeatherIconsView(drift: $cloudDrift) + .frame(height: 150) + .padding(.top, 15) + + VStack(spacing: 16) { + Text("Your Emotional\nForecast") + .font(.system(size: 34, weight: .bold, design: .rounded)) + .multilineTextAlignment(.center) + .foregroundStyle( + LinearGradient( + colors: [ + Color(red: 0.95, green: 0.95, blue: 1.0), + Color(red: 0.7, green: 0.85, blue: 1.0) + ], + startPoint: .top, + endPoint: .bottom + ) + ) + + Text("Predict your patterns.\nPrepare for any weather.") + .font(.system(size: 16, weight: .medium, design: .rounded)) + .multilineTextAlignment(.center) + .foregroundColor(.white.opacity(0.75)) + .lineSpacing(4) + } + .padding(.horizontal, 32) + .opacity(showContent ? 1 : 0) + .offset(y: showContent ? 0 : 20) + + FeatureCardsGrid(style: .forecast) + .padding(.top, 28) + .padding(.horizontal, 28) + .opacity(showContent ? 1 : 0) + + SocialProofBadge(style: .forecast) + .padding(.top, 24) + .opacity(showContent ? 1 : 0) + + Spacer().frame(height: 20) + } + } + .onAppear { + withAnimation(.easeInOut(duration: 4).repeatForever(autoreverses: true)) { + cloudDrift = true + } + withAnimation(.easeOut(duration: 0.8).delay(0.2)) { + showContent = true + } + } + } +} + +// MARK: - 11. Playful Theme (Vibrant & Fun) + +struct PlayfulMarketingContent: View { + @State private var showContent = false + @State private var bounce = false + + var body: some View { + ZStack { + PlayfulBackground(bounce: $bounce) + + VStack(spacing: 0) { + // Bouncing emoji faces + PlayfulEmojisView(bounce: $bounce) + .frame(height: 150) + .padding(.top, 15) + + VStack(spacing: 16) { + Text("MOOD UNLOCKED! ๐ŸŽฎ") + .font(.system(size: 30, weight: .black, design: .rounded)) + .foregroundStyle( + LinearGradient( + colors: [ + Color(red: 0.2, green: 1.0, blue: 0.4), + Color(red: 1.0, green: 1.0, blue: 0.2) + ], + startPoint: .leading, + endPoint: .trailing + ) + ) + .shadow(color: Color(red: 0.2, green: 1.0, blue: 0.4).opacity(0.5), radius: 15) + + Text("Level up your self-awareness.\nPremium = MAX POWER!") + .font(.system(size: 15, weight: .bold, design: .rounded)) + .multilineTextAlignment(.center) + .foregroundColor(.white.opacity(0.85)) + .lineSpacing(4) + } + .padding(.horizontal, 32) + .opacity(showContent ? 1 : 0) + .offset(y: showContent ? 0 : 20) + + FeatureCardsGrid(style: .playful) + .padding(.top, 28) + .padding(.horizontal, 28) + .opacity(showContent ? 1 : 0) + + SocialProofBadge(style: .playful) + .padding(.top, 24) + .opacity(showContent ? 1 : 0) + + Spacer().frame(height: 20) + } + } + .onAppear { + withAnimation(.easeInOut(duration: 0.6).repeatForever(autoreverses: true)) { + bounce = true + } + withAnimation(.easeOut(duration: 0.6).delay(0.1)) { + showContent = true + } + } + } +} + +// MARK: - 12. Journal Theme (Handwritten Paper) + +struct JournalMarketingContent: View { + @State private var showContent = false + @State private var pageFlip = false + + var body: some View { + ZStack { + JournalBackground() + + VStack(spacing: 0) { + // Stacked paper pages + JournalPagesView(flip: $pageFlip) + .frame(height: 150) + .padding(.top, 15) + + VStack(spacing: 16) { + Text("Your Personal\nDiary") + .font(.custom("Georgia", size: 34)) + .italic() + .multilineTextAlignment(.center) + .foregroundColor(Color(red: 0.35, green: 0.25, blue: 0.2)) + + Text("Every entry tells your story.\nPremium unlocks the full narrative.") + .font(.custom("Georgia", size: 15)) + .multilineTextAlignment(.center) + .foregroundColor(Color(red: 0.5, green: 0.4, blue: 0.35)) + .lineSpacing(6) + } + .padding(.horizontal, 40) + .opacity(showContent ? 1 : 0) + .offset(y: showContent ? 0 : 15) + + FeatureCardsGrid(style: .journal) + .padding(.top, 28) + .padding(.horizontal, 28) + .opacity(showContent ? 1 : 0) + + SocialProofBadge(style: .journal) + .padding(.top, 24) + .opacity(showContent ? 1 : 0) + + Spacer().frame(height: 20) + } + } + .onAppear { + withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) { + pageFlip = true + } + withAnimation(.easeOut(duration: 1.0).delay(0.2)) { + showContent = true + } + } + } +} + // MARK: - Background Views struct CelestialBackground: View { @@ -575,6 +1111,266 @@ struct MinimalBackground: View { } } +struct ZenBackground: View { + @Binding var inkFlow: Bool + + var body: some View { + ZStack { + // Dark paper texture + LinearGradient( + colors: [ + Color(red: 0.12, green: 0.1, blue: 0.08), + Color(red: 0.08, green: 0.07, blue: 0.06), + Color(red: 0.1, green: 0.08, blue: 0.06) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Subtle ink wash + EllipticalGradient( + colors: [ + Color(red: 0.3, green: 0.4, blue: 0.35).opacity(inkFlow ? 0.2 : 0.1), + Color.clear + ], + center: .center, + startRadiusFraction: 0, + endRadiusFraction: 0.7 + ) + .frame(width: 400, height: 400) + .blur(radius: 80) + } + .ignoresSafeArea() + } +} + +struct EditorialBackground: View { + var body: some View { + ZStack { + // Clean off-white + LinearGradient( + colors: [ + Color(red: 0.98, green: 0.97, blue: 0.95), + Color(red: 0.96, green: 0.95, blue: 0.93), + Color(red: 0.94, green: 0.93, blue: 0.91) + ], + startPoint: .top, + endPoint: .bottom + ) + } + .ignoresSafeArea() + } +} + +struct MixtapeBackground: View { + var body: some View { + ZStack { + // Warm brown gradient + LinearGradient( + colors: [ + Color(red: 0.15, green: 0.1, blue: 0.08), + Color(red: 0.12, green: 0.08, blue: 0.06), + Color(red: 0.1, green: 0.07, blue: 0.05) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Warm orange glow + EllipticalGradient( + colors: [ + Color(red: 0.8, green: 0.4, blue: 0.2).opacity(0.15), + Color.clear + ], + center: .center, + startRadiusFraction: 0, + endRadiusFraction: 0.6 + ) + .frame(width: 400, height: 300) + .offset(y: -50) + .blur(radius: 60) + } + .ignoresSafeArea() + } +} + +struct HeartfeltBackground: View { + @Binding var beat: Bool + + var body: some View { + ZStack { + // Warm pink gradient + LinearGradient( + colors: [ + Color(red: 0.15, green: 0.05, blue: 0.08), + Color(red: 0.12, green: 0.04, blue: 0.06), + Color(red: 0.1, green: 0.03, blue: 0.05) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Pink glow + EllipticalGradient( + colors: [ + Color(red: 1.0, green: 0.3, blue: 0.5).opacity(beat ? 0.25 : 0.15), + Color.clear + ], + center: .center, + startRadiusFraction: 0, + endRadiusFraction: 0.7 + ) + .frame(width: 400, height: 400) + .blur(radius: 80) + } + .ignoresSafeArea() + } +} + +struct LuxeBackground: View { + @Binding var shimmer: Bool + + var body: some View { + ZStack { + // Deep blue-black + LinearGradient( + colors: [ + Color(red: 0.06, green: 0.08, blue: 0.12), + Color(red: 0.04, green: 0.05, blue: 0.08), + Color(red: 0.03, green: 0.04, blue: 0.06) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Subtle blue shimmer + EllipticalGradient( + colors: [ + Color(red: 0.4, green: 0.5, blue: 0.8).opacity(shimmer ? 0.2 : 0.1), + Color.clear + ], + center: .center, + startRadiusFraction: 0, + endRadiusFraction: 0.6 + ) + .frame(width: 400, height: 300) + .offset(y: shimmer ? -30 : -60) + .blur(radius: 60) + } + .ignoresSafeArea() + } +} + +struct ForecastBackground: View { + @Binding var drift: Bool + + var body: some View { + ZStack { + // Sky gradient + LinearGradient( + colors: [ + Color(red: 0.15, green: 0.25, blue: 0.4), + Color(red: 0.1, green: 0.18, blue: 0.3), + Color(red: 0.08, green: 0.12, blue: 0.2) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Cloud-like glow + EllipticalGradient( + colors: [ + Color.white.opacity(0.1), + Color.clear + ], + center: .center, + startRadiusFraction: 0, + endRadiusFraction: 0.5 + ) + .frame(width: 350, height: 200) + .offset(x: drift ? 30 : -30, y: -80) + .blur(radius: 50) + } + .ignoresSafeArea() + } +} + +struct PlayfulBackground: View { + @Binding var bounce: Bool + + var body: some View { + ZStack { + // Deep purple-black + LinearGradient( + colors: [ + Color(red: 0.08, green: 0.05, blue: 0.12), + Color(red: 0.05, green: 0.03, blue: 0.08), + Color(red: 0.04, green: 0.02, blue: 0.06) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Green glow + EllipticalGradient( + colors: [ + Color(red: 0.2, green: 1.0, blue: 0.4).opacity(bounce ? 0.2 : 0.1), + Color.clear + ], + center: .center, + startRadiusFraction: 0, + endRadiusFraction: 0.5 + ) + .frame(width: 300, height: 300) + .offset(x: -50, y: -50) + .blur(radius: 60) + + // Yellow glow + EllipticalGradient( + colors: [ + Color(red: 1.0, green: 1.0, blue: 0.2).opacity(0.1), + Color.clear + ], + center: .center, + startRadiusFraction: 0, + endRadiusFraction: 0.4 + ) + .frame(width: 250, height: 250) + .offset(x: 60, y: 100) + .blur(radius: 50) + } + .ignoresSafeArea() + } +} + +struct JournalBackground: View { + var body: some View { + ZStack { + // Warm paper + LinearGradient( + colors: [ + Color(red: 1.0, green: 0.98, blue: 0.94), + Color(red: 0.98, green: 0.95, blue: 0.88), + Color(red: 0.95, green: 0.92, blue: 0.85) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Paper texture lines + VStack(spacing: 28) { + ForEach(0..<20, id: \.self) { _ in + Rectangle() + .fill(Color(red: 0.85, green: 0.82, blue: 0.78).opacity(0.3)) + .frame(height: 1) + } + } + .padding(.horizontal, 40) + } + .ignoresSafeArea() + } +} + // MARK: - Decorative Elements struct EmotionOrbsView: View { @@ -827,6 +1623,264 @@ struct MinimalBreathingCircle: View { } } +// MARK: - Theme-Specific Decorative Elements + +struct ZenEnsoView: View { + @Binding var animate: Bool + + var body: some View { + ZStack { + // Ensล circle (imperfect zen circle) + Circle() + .trim(from: 0, to: animate ? 0.85 : 0.8) + .stroke( + LinearGradient( + colors: [ + Color(red: 0.6, green: 0.55, blue: 0.5), + Color(red: 0.4, green: 0.35, blue: 0.3) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + style: StrokeStyle(lineWidth: 8, lineCap: .round) + ) + .frame(width: 100, height: 100) + .rotationEffect(.degrees(-90)) + .shadow(color: Color(red: 0.6, green: 0.55, blue: 0.5).opacity(0.3), radius: 10) + + // Inner dot + Circle() + .fill(Color(red: 0.5, green: 0.45, blue: 0.4)) + .frame(width: 8, height: 8) + } + } +} + +struct CassetteTapeView: View { + @Binding var rotating: Bool + + var body: some View { + ZStack { + // Tape body + RoundedRectangle(cornerRadius: 8) + .fill(Color(red: 0.25, green: 0.2, blue: 0.15)) + .frame(width: 140, height: 90) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(red: 0.4, green: 0.3, blue: 0.2), lineWidth: 2) + ) + + // Tape window + RoundedRectangle(cornerRadius: 4) + .fill(Color(red: 0.15, green: 0.12, blue: 0.1)) + .frame(width: 100, height: 40) + + // Reels + HStack(spacing: 40) { + Circle() + .fill(Color(red: 0.3, green: 0.25, blue: 0.2)) + .frame(width: 30, height: 30) + .overlay( + Circle() + .stroke(Color(red: 0.5, green: 0.4, blue: 0.3), lineWidth: 1) + ) + .rotationEffect(.degrees(rotating ? 360 : 0)) + + Circle() + .fill(Color(red: 0.3, green: 0.25, blue: 0.2)) + .frame(width: 30, height: 30) + .overlay( + Circle() + .stroke(Color(red: 0.5, green: 0.4, blue: 0.3), lineWidth: 1) + ) + .rotationEffect(.degrees(rotating ? 360 : 0)) + } + + // Label + Text("FEELS MIXTAPE") + .font(.system(size: 8, weight: .bold, design: .monospaced)) + .foregroundColor(Color(red: 0.9, green: 0.8, blue: 0.6)) + .offset(y: 32) + } + } +} + +struct FloatingHeartsView: View { + @Binding var beat: Bool + + var body: some View { + ZStack { + ForEach(0..<5, id: \.self) { index in + Image(systemName: "heart.fill") + .font(.system(size: CGFloat(20 + index * 8))) + .foregroundStyle( + LinearGradient( + colors: [ + Color(red: 1.0, green: 0.5 + Double(index) * 0.1, blue: 0.6 + Double(index) * 0.05), + Color(red: 1.0, green: 0.3, blue: 0.4) + ], + startPoint: .top, + endPoint: .bottom + ) + ) + .offset( + x: CGFloat(index * 25 - 50), + y: CGFloat(index % 2 == 0 ? -10 : 10) + (beat ? -5 : 5) + ) + .shadow(color: Color(red: 1.0, green: 0.4, blue: 0.5).opacity(0.4), radius: 10) + .scaleEffect(beat ? 1.1 : 0.95) + } + } + } +} + +struct LuxeGemView: View { + @Binding var shimmer: Bool + + var body: some View { + ZStack { + // Outer glow + Circle() + .fill( + RadialGradient( + colors: [ + Color(red: 0.6, green: 0.7, blue: 1.0).opacity(shimmer ? 0.3 : 0.15), + Color.clear + ], + center: .center, + startRadius: 0, + endRadius: 60 + ) + ) + .frame(width: 120, height: 120) + + // Diamond shape + Image(systemName: "diamond.fill") + .font(.system(size: 60)) + .foregroundStyle( + LinearGradient( + colors: [ + Color.white, + Color(red: 0.8, green: 0.85, blue: 1.0), + Color(red: 0.6, green: 0.7, blue: 0.9) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .shadow(color: Color(red: 0.6, green: 0.7, blue: 1.0).opacity(0.5), radius: shimmer ? 20 : 10) + .scaleEffect(shimmer ? 1.05 : 1.0) + } + } +} + +struct WeatherIconsView: View { + @Binding var drift: Bool + + var body: some View { + HStack(spacing: 20) { + Image(systemName: "cloud.sun.fill") + .font(.system(size: 40)) + .foregroundStyle( + LinearGradient( + colors: [Color.yellow, Color.orange], + startPoint: .top, + endPoint: .bottom + ) + ) + .offset(y: drift ? -5 : 5) + + Image(systemName: "sun.max.fill") + .font(.system(size: 50)) + .foregroundStyle( + LinearGradient( + colors: [Color.yellow, Color.orange], + startPoint: .top, + endPoint: .bottom + ) + ) + .shadow(color: Color.yellow.opacity(0.5), radius: 15) + + Image(systemName: "cloud.rain.fill") + .font(.system(size: 40)) + .foregroundStyle( + LinearGradient( + colors: [Color.white, Color.blue.opacity(0.7)], + startPoint: .top, + endPoint: .bottom + ) + ) + .offset(y: drift ? 5 : -5) + } + } +} + +struct PlayfulEmojisView: View { + @Binding var bounce: Bool + + var body: some View { + HStack(spacing: 15) { + Text("๐Ÿ˜„") + .font(.system(size: 40)) + .offset(y: bounce ? -10 : 0) + + Text("๐ŸŽ‰") + .font(.system(size: 35)) + .offset(y: bounce ? 0 : -10) + + Text("โœจ") + .font(.system(size: 45)) + .offset(y: bounce ? -15 : 5) + + Text("๐ŸŒˆ") + .font(.system(size: 35)) + .offset(y: bounce ? 5 : -5) + + Text("๐Ÿ’ซ") + .font(.system(size: 40)) + .offset(y: bounce ? -5 : 10) + } + } +} + +struct JournalPagesView: View { + @Binding var flip: Bool + + var body: some View { + ZStack { + // Back pages + ForEach(0..<3, id: \.self) { index in + RoundedRectangle(cornerRadius: 4) + .fill(Color(red: 1.0, green: 0.98, blue: 0.94)) + .frame(width: 100 - CGFloat(index * 5), height: 120 - CGFloat(index * 5)) + .offset(x: CGFloat(index * 3), y: CGFloat(index * 3)) + .shadow(color: Color.black.opacity(0.1), radius: 2, x: 1, y: 1) + } + + // Front page with lines + RoundedRectangle(cornerRadius: 4) + .fill(Color(red: 1.0, green: 0.98, blue: 0.94)) + .frame(width: 100, height: 120) + .overlay( + VStack(spacing: 10) { + ForEach(0..<6, id: \.self) { _ in + Rectangle() + .fill(Color(red: 0.8, green: 0.75, blue: 0.7).opacity(0.5)) + .frame(height: 1) + } + } + .padding(.horizontal, 12) + .padding(.vertical, 15) + ) + .shadow(color: Color.black.opacity(0.15), radius: 4, x: 2, y: 2) + .rotation3DEffect( + .degrees(flip ? 5 : -5), + axis: (x: 0, y: 1, z: 0) + ) + } + } +} + // MARK: - Feature Cards struct FeatureCardsGrid: View { @@ -907,27 +1961,52 @@ struct FeatureCard: View { private var accentColor: Color { let colors: [[Color]] = [ - // Celestial + // 0: Celestial [Color(red: 1.0, green: 0.6, blue: 0.4), Color(red: 0.6, green: 0.5, blue: 1.0), Color(red: 1.0, green: 0.4, blue: 0.5), Color(red: 0.4, green: 0.8, blue: 0.7)], - // Garden + // 1: Garden [Color(red: 0.5, green: 0.8, blue: 0.5), Color(red: 0.7, green: 0.6, blue: 0.9), Color(red: 1.0, green: 0.7, blue: 0.7), Color(red: 0.6, green: 0.75, blue: 0.5)], - // Neon + // 2: Neon [Color(red: 0.0, green: 1.0, blue: 0.8), Color(red: 1.0, green: 0.0, blue: 0.8), Color(red: 1.0, green: 1.0, blue: 0.0), Color(red: 0.5, green: 0.0, blue: 1.0)], - // Minimal + // 3: Minimal [Color(red: 0.85, green: 0.55, blue: 0.45), Color(red: 0.6, green: 0.5, blue: 0.45), - Color(red: 0.75, green: 0.5, blue: 0.5), Color(red: 0.65, green: 0.6, blue: 0.5)] + Color(red: 0.75, green: 0.5, blue: 0.5), Color(red: 0.65, green: 0.6, blue: 0.5)], + // 4: Zen + [Color(red: 0.5, green: 0.6, blue: 0.55), Color(red: 0.6, green: 0.55, blue: 0.5), + Color(red: 0.55, green: 0.5, blue: 0.45), Color(red: 0.5, green: 0.55, blue: 0.5)], + // 5: Editorial + [Color(red: 0.3, green: 0.3, blue: 0.35), Color(red: 0.4, green: 0.35, blue: 0.3), + Color(red: 0.35, green: 0.3, blue: 0.35), Color(red: 0.3, green: 0.35, blue: 0.4)], + // 6: Mixtape + [Color(red: 0.9, green: 0.6, blue: 0.3), Color(red: 0.8, green: 0.5, blue: 0.3), + Color(red: 0.85, green: 0.55, blue: 0.35), Color(red: 0.75, green: 0.5, blue: 0.25)], + // 7: Heartfelt + [Color(red: 1.0, green: 0.5, blue: 0.6), Color(red: 1.0, green: 0.4, blue: 0.5), + Color(red: 1.0, green: 0.55, blue: 0.65), Color(red: 0.95, green: 0.45, blue: 0.55)], + // 8: Luxe + [Color(red: 0.7, green: 0.8, blue: 1.0), Color(red: 0.8, green: 0.85, blue: 0.95), + Color(red: 0.75, green: 0.8, blue: 0.9), Color(red: 0.65, green: 0.75, blue: 0.95)], + // 9: Forecast + [Color(red: 0.4, green: 0.7, blue: 1.0), Color(red: 1.0, green: 0.85, blue: 0.3), + Color(red: 0.5, green: 0.8, blue: 0.9), Color(red: 0.9, green: 0.6, blue: 0.4)], + // 10: Playful + [Color(red: 0.2, green: 1.0, blue: 0.4), Color(red: 1.0, green: 1.0, blue: 0.2), + Color(red: 1.0, green: 0.4, blue: 0.2), Color(red: 0.6, green: 0.2, blue: 1.0)], + // 11: Journal + [Color(red: 0.6, green: 0.45, blue: 0.35), Color(red: 0.5, green: 0.4, blue: 0.35), + Color(red: 0.55, green: 0.45, blue: 0.4), Color(red: 0.5, green: 0.4, blue: 0.3)] ] - return colors[style.rawValue][accentIndex] + let safeIndex = min(style.rawValue, colors.count - 1) + return colors[safeIndex][accentIndex] } private var titleFont: Font { switch style { - case .neon: + case .neon, .playful: return .system(size: 14, weight: .bold, design: .monospaced) - case .minimal: + case .minimal, .editorial, .zen, .journal: return .system(size: 14, weight: .medium, design: .serif) default: return .system(size: 14, weight: .bold, design: .rounded) @@ -935,17 +2014,27 @@ struct FeatureCard: View { } private var titleColor: Color { - style == .minimal ? Color(red: 0.2, green: 0.15, blue: 0.1) : .white + switch style { + case .minimal, .editorial, .journal: + return Color(red: 0.2, green: 0.15, blue: 0.1) + default: + return .white + } } private var subtitleColor: Color { - style == .minimal ? Color(red: 0.5, green: 0.45, blue: 0.4) : .white.opacity(0.6) + switch style { + case .minimal, .editorial, .journal: + return Color(red: 0.5, green: 0.45, blue: 0.4) + default: + return .white.opacity(0.6) + } } @ViewBuilder private var cardBackground: some View { switch style { - case .celestial, .garden: + case .celestial, .garden, .heartfelt, .forecast, .luxe: RoundedRectangle(cornerRadius: 16) .fill(.ultraThinMaterial) .overlay( @@ -962,7 +2051,7 @@ struct FeatureCard: View { RoundedRectangle(cornerRadius: 16) .stroke(.white.opacity(0.15), lineWidth: 1) ) - case .neon: + case .neon, .playful: RoundedRectangle(cornerRadius: 12) .fill(Color.black.opacity(0.5)) .overlay( @@ -979,7 +2068,7 @@ struct FeatureCard: View { lineWidth: 1 ) ) - case .minimal: + case .minimal, .editorial: RoundedRectangle(cornerRadius: 14) .fill(Color.white.opacity(0.7)) .overlay( @@ -987,6 +2076,28 @@ struct FeatureCard: View { .stroke(Color(red: 0.85, green: 0.82, blue: 0.78), lineWidth: 1) ) .shadow(color: Color.black.opacity(0.04), radius: 8, x: 0, y: 4) + case .zen: + RoundedRectangle(cornerRadius: 12) + .fill(Color.black.opacity(0.3)) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color(red: 0.5, green: 0.45, blue: 0.4).opacity(0.3), lineWidth: 1) + ) + case .mixtape: + RoundedRectangle(cornerRadius: 8) + .fill(Color(red: 0.2, green: 0.15, blue: 0.1).opacity(0.8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(red: 0.5, green: 0.35, blue: 0.2), lineWidth: 1) + ) + case .journal: + RoundedRectangle(cornerRadius: 6) + .fill(Color(red: 1.0, green: 0.98, blue: 0.94)) + .overlay( + RoundedRectangle(cornerRadius: 6) + .stroke(Color(red: 0.8, green: 0.75, blue: 0.7), lineWidth: 1) + ) + .shadow(color: Color.black.opacity(0.08), radius: 4, x: 1, y: 2) } } } @@ -1029,16 +2140,22 @@ struct SocialProofBadge: View { switch style { case .celestial: return Color(red: 0.08, green: 0.06, blue: 0.15) case .garden: return Color(red: 0.05, green: 0.12, blue: 0.08) - case .neon: return Color(red: 0.02, green: 0.02, blue: 0.05) - case .minimal: return Color(red: 0.95, green: 0.93, blue: 0.9) + case .neon, .playful: return Color(red: 0.02, green: 0.02, blue: 0.05) + case .minimal, .editorial: return Color(red: 0.95, green: 0.93, blue: 0.9) + case .zen: return Color(red: 0.1, green: 0.08, blue: 0.06) + case .mixtape: return Color(red: 0.12, green: 0.08, blue: 0.06) + case .heartfelt: return Color(red: 0.12, green: 0.04, blue: 0.06) + case .luxe: return Color(red: 0.04, green: 0.05, blue: 0.08) + case .forecast: return Color(red: 0.1, green: 0.18, blue: 0.3) + case .journal: return Color(red: 0.98, green: 0.95, blue: 0.88) } } private var textFont: Font { switch style { - case .neon: + case .neon, .playful: return .system(size: 12, weight: .medium, design: .monospaced) - case .minimal: + case .minimal, .editorial, .zen, .journal: return .system(size: 12, weight: .regular, design: .serif) default: return .system(size: 12, weight: .medium, design: .rounded) @@ -1046,17 +2163,22 @@ struct SocialProofBadge: View { } private var textColor: Color { - style == .minimal ? Color(red: 0.5, green: 0.45, blue: 0.4) : .white.opacity(0.7) + switch style { + case .minimal, .editorial, .journal: + return Color(red: 0.5, green: 0.45, blue: 0.4) + default: + return .white.opacity(0.7) + } } @ViewBuilder private var badgeBackground: some View { switch style { - case .minimal: + case .minimal, .editorial: Capsule() .fill(Color.white.opacity(0.8)) .overlay(Capsule().stroke(Color(red: 0.85, green: 0.82, blue: 0.78), lineWidth: 1)) - case .neon: + case .neon, .playful: Capsule() .fill(Color.black.opacity(0.6)) .overlay( @@ -1073,6 +2195,18 @@ struct SocialProofBadge: View { lineWidth: 1 ) ) + case .journal: + Capsule() + .fill(Color(red: 1.0, green: 0.98, blue: 0.94)) + .overlay(Capsule().stroke(Color(red: 0.8, green: 0.75, blue: 0.7), lineWidth: 1)) + case .zen: + Capsule() + .fill(Color.black.opacity(0.3)) + .overlay(Capsule().stroke(Color(red: 0.5, green: 0.45, blue: 0.4).opacity(0.3), lineWidth: 1)) + case .mixtape: + Capsule() + .fill(Color(red: 0.2, green: 0.15, blue: 0.1).opacity(0.8)) + .overlay(Capsule().stroke(Color(red: 0.5, green: 0.35, blue: 0.2), lineWidth: 1)) default: Capsule() .fill(.ultraThinMaterial) @@ -1082,36 +2216,93 @@ struct SocialProofBadge: View { private func avatarColors(for index: Int) -> [Color] { let colorSets: [[[Color]]] = [ - // Celestial + // 0: Celestial [ [Color(red: 1.0, green: 0.6, blue: 0.4), Color(red: 1.0, green: 0.4, blue: 0.3)], [Color(red: 0.4, green: 0.7, blue: 0.9), Color(red: 0.3, green: 0.5, blue: 0.8)], [Color(red: 0.6, green: 0.8, blue: 0.5), Color(red: 0.4, green: 0.7, blue: 0.4)], [Color(red: 0.8, green: 0.5, blue: 0.9), Color(red: 0.6, green: 0.3, blue: 0.8)] ], - // Garden + // 1: Garden [ [Color(red: 0.6, green: 0.8, blue: 0.5), Color(red: 0.4, green: 0.7, blue: 0.4)], [Color(red: 1.0, green: 0.7, blue: 0.7), Color(red: 0.9, green: 0.5, blue: 0.5)], [Color(red: 0.7, green: 0.6, blue: 0.9), Color(red: 0.5, green: 0.4, blue: 0.7)], [Color(red: 1.0, green: 0.85, blue: 0.5), Color(red: 0.9, green: 0.7, blue: 0.3)] ], - // Neon + // 2: Neon [ [Color(red: 0.0, green: 1.0, blue: 0.8), Color(red: 0.0, green: 0.7, blue: 0.6)], [Color(red: 1.0, green: 0.0, blue: 0.8), Color(red: 0.7, green: 0.0, blue: 0.6)], [Color(red: 1.0, green: 1.0, blue: 0.0), Color(red: 0.8, green: 0.8, blue: 0.0)], [Color(red: 0.5, green: 0.0, blue: 1.0), Color(red: 0.3, green: 0.0, blue: 0.7)] ], - // Minimal + // 3: Minimal [ [Color(red: 0.85, green: 0.7, blue: 0.65), Color(red: 0.75, green: 0.6, blue: 0.55)], [Color(red: 0.7, green: 0.65, blue: 0.6), Color(red: 0.6, green: 0.55, blue: 0.5)], [Color(red: 0.8, green: 0.65, blue: 0.6), Color(red: 0.7, green: 0.55, blue: 0.5)], [Color(red: 0.75, green: 0.7, blue: 0.65), Color(red: 0.65, green: 0.6, blue: 0.55)] + ], + // 4: Zen + [ + [Color(red: 0.5, green: 0.55, blue: 0.5), Color(red: 0.4, green: 0.45, blue: 0.4)], + [Color(red: 0.55, green: 0.5, blue: 0.45), Color(red: 0.45, green: 0.4, blue: 0.35)], + [Color(red: 0.5, green: 0.5, blue: 0.5), Color(red: 0.4, green: 0.4, blue: 0.4)], + [Color(red: 0.55, green: 0.55, blue: 0.5), Color(red: 0.45, green: 0.45, blue: 0.4)] + ], + // 5: Editorial + [ + [Color(red: 0.4, green: 0.4, blue: 0.45), Color(red: 0.3, green: 0.3, blue: 0.35)], + [Color(red: 0.45, green: 0.4, blue: 0.4), Color(red: 0.35, green: 0.3, blue: 0.3)], + [Color(red: 0.4, green: 0.45, blue: 0.45), Color(red: 0.3, green: 0.35, blue: 0.35)], + [Color(red: 0.45, green: 0.45, blue: 0.4), Color(red: 0.35, green: 0.35, blue: 0.3)] + ], + // 6: Mixtape + [ + [Color(red: 0.8, green: 0.5, blue: 0.3), Color(red: 0.6, green: 0.35, blue: 0.2)], + [Color(red: 0.7, green: 0.45, blue: 0.25), Color(red: 0.5, green: 0.3, blue: 0.15)], + [Color(red: 0.85, green: 0.55, blue: 0.35), Color(red: 0.65, green: 0.4, blue: 0.25)], + [Color(red: 0.75, green: 0.5, blue: 0.3), Color(red: 0.55, green: 0.35, blue: 0.2)] + ], + // 7: Heartfelt + [ + [Color(red: 1.0, green: 0.5, blue: 0.6), Color(red: 0.9, green: 0.4, blue: 0.5)], + [Color(red: 1.0, green: 0.6, blue: 0.65), Color(red: 0.9, green: 0.5, blue: 0.55)], + [Color(red: 1.0, green: 0.45, blue: 0.55), Color(red: 0.9, green: 0.35, blue: 0.45)], + [Color(red: 1.0, green: 0.55, blue: 0.6), Color(red: 0.9, green: 0.45, blue: 0.5)] + ], + // 8: Luxe + [ + [Color(red: 0.7, green: 0.8, blue: 1.0), Color(red: 0.5, green: 0.6, blue: 0.8)], + [Color(red: 0.8, green: 0.85, blue: 0.95), Color(red: 0.6, green: 0.65, blue: 0.75)], + [Color(red: 0.75, green: 0.8, blue: 0.9), Color(red: 0.55, green: 0.6, blue: 0.7)], + [Color(red: 0.65, green: 0.75, blue: 0.95), Color(red: 0.45, green: 0.55, blue: 0.75)] + ], + // 9: Forecast + [ + [Color(red: 0.4, green: 0.7, blue: 1.0), Color(red: 0.3, green: 0.5, blue: 0.8)], + [Color(red: 1.0, green: 0.85, blue: 0.3), Color(red: 0.8, green: 0.65, blue: 0.2)], + [Color(red: 0.5, green: 0.8, blue: 0.9), Color(red: 0.4, green: 0.6, blue: 0.7)], + [Color(red: 0.9, green: 0.6, blue: 0.4), Color(red: 0.7, green: 0.4, blue: 0.3)] + ], + // 10: Playful + [ + [Color(red: 0.2, green: 1.0, blue: 0.4), Color(red: 0.1, green: 0.8, blue: 0.3)], + [Color(red: 1.0, green: 1.0, blue: 0.2), Color(red: 0.8, green: 0.8, blue: 0.1)], + [Color(red: 1.0, green: 0.4, blue: 0.2), Color(red: 0.8, green: 0.3, blue: 0.1)], + [Color(red: 0.6, green: 0.2, blue: 1.0), Color(red: 0.4, green: 0.1, blue: 0.8)] + ], + // 11: Journal + [ + [Color(red: 0.6, green: 0.45, blue: 0.35), Color(red: 0.5, green: 0.35, blue: 0.25)], + [Color(red: 0.55, green: 0.4, blue: 0.3), Color(red: 0.45, green: 0.3, blue: 0.2)], + [Color(red: 0.5, green: 0.45, blue: 0.4), Color(red: 0.4, green: 0.35, blue: 0.3)], + [Color(red: 0.55, green: 0.45, blue: 0.35), Color(red: 0.45, green: 0.35, blue: 0.25)] ] ] - return colorSets[style.rawValue][index % 4] + let safeIndex = min(style.rawValue, colorSets.count - 1) + return colorSets[safeIndex][index % 4] } } diff --git a/Shared/Views/LockScreenView.swift b/Shared/Views/LockScreenView.swift index 746525c..c3cc061 100644 --- a/Shared/Views/LockScreenView.swift +++ b/Shared/Views/LockScreenView.swift @@ -3,11 +3,35 @@ // Feels // // Lock screen shown when privacy lock is enabled and app needs authentication. -// Design: "Emotional Aurora" - A sanctuary for your feelings +// Supports multiple themed styles that match app themes. // import SwiftUI +// MARK: - Lock Screen Style Protocol + +protocol LockScreenTheme { + var backgroundColor: AnyView { get } + var centralElement: AnyView { get } + var titleText: String { get } + var subtitleText: String { get } + var taglineText: String { get } + var titleFont: Font { get } + var subtitleFont: Font { get } + var taglineFont: Font { get } + func titleColor(isDark: Bool) -> Color + func subtitleColor(isDark: Bool) -> Color + func taglineColor(isDark: Bool) -> Color + func buttonStyle(isDark: Bool) -> LockButtonStyle +} + +struct LockButtonStyle { + let backgroundColor: Color + let foregroundColor: Color + let borderColor: Color + let useMaterial: Bool +} + // MARK: - Floating Mood Particle struct MoodParticle: Identifiable { @@ -295,6 +319,1016 @@ struct BreathingOrb: View { } } +// MARK: - Zen Lock Screen Theme + +struct ZenLockBackground: View { + @Environment(\.colorScheme) private var colorScheme + @State private var breathe = false + + var body: some View { + ZStack { + // Warm paper background + LinearGradient( + colors: colorScheme == .dark ? [ + Color(red: 0.12, green: 0.11, blue: 0.10), + Color(red: 0.08, green: 0.07, blue: 0.06) + ] : [ + Color(red: 0.96, green: 0.94, blue: 0.90), + Color(red: 0.92, green: 0.90, blue: 0.86) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Subtle ink wash effect + Circle() + .fill( + RadialGradient( + colors: colorScheme == .dark ? [ + Color(red: 0.4, green: 0.45, blue: 0.4).opacity(0.15), + .clear + ] : [ + Color(red: 0.3, green: 0.35, blue: 0.3).opacity(0.08), + .clear + ], + center: .center, + startRadius: 0, + endRadius: 300 + ) + ) + .scaleEffect(breathe ? 1.1 : 1.0) + .blur(radius: 80) + } + .ignoresSafeArea() + .onAppear { + withAnimation(.easeInOut(duration: 6).repeatForever(autoreverses: true)) { + breathe = true + } + } + } +} + +struct ZenEnsoOrb: View { + @Environment(\.colorScheme) private var colorScheme + @State private var drawProgress: CGFloat = 0 + @State private var breathe = false + + var body: some View { + ZStack { + // Outer glow + Circle() + .stroke( + Color(red: 0.35, green: 0.4, blue: 0.35).opacity(colorScheme == .dark ? 0.3 : 0.15), + lineWidth: 4 + ) + .frame(width: 140, height: 140) + .blur(radius: 15) + .scaleEffect(breathe ? 1.1 : 0.95) + + // Enso circle - incomplete for zen aesthetics + Circle() + .trim(from: 0, to: 0.85) + .stroke( + Color(red: 0.3, green: 0.35, blue: 0.3), + style: StrokeStyle(lineWidth: 5, lineCap: .round) + ) + .frame(width: 100, height: 100) + .rotationEffect(.degrees(-90)) + .scaleEffect(breathe ? 1.02 : 0.98) + } + .onAppear { + withAnimation(.easeInOut(duration: 5).repeatForever(autoreverses: true)) { + breathe = true + } + } + } +} + +// MARK: - Neon Lock Screen Theme + +struct NeonLockBackground: View { + @State private var pulse = false + + var body: some View { + ZStack { + // Deep black base + Color(red: 0.02, green: 0.02, blue: 0.05) + + // Grid lines + Canvas { context, size in + let spacing: CGFloat = 30 + for y in stride(from: 0, to: size.height, by: spacing) { + var path = Path() + path.move(to: CGPoint(x: 0, y: y)) + path.addLine(to: CGPoint(x: size.width, y: y)) + context.stroke(path, with: .color(Color.cyan.opacity(0.08)), lineWidth: 0.5) + } + for x in stride(from: 0, to: size.width, by: spacing) { + var path = Path() + path.move(to: CGPoint(x: x, y: 0)) + path.addLine(to: CGPoint(x: x, y: size.height)) + context.stroke(path, with: .color(Color.cyan.opacity(0.08)), lineWidth: 0.5) + } + } + + // Neon glow spots + Circle() + .fill(Color(red: 0, green: 1, blue: 0.82).opacity(pulse ? 0.3 : 0.15)) + .frame(width: 300, height: 300) + .blur(radius: 80) + .offset(y: -100) + + Circle() + .fill(Color(red: 1, green: 0, blue: 0.8).opacity(pulse ? 0.2 : 0.1)) + .frame(width: 250, height: 250) + .blur(radius: 70) + .offset(x: 50, y: 150) + } + .ignoresSafeArea() + .onAppear { + withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) { + pulse = true + } + } + } +} + +struct NeonRingOrb: View { + @State private var rotate = false + @State private var pulse = false + + var body: some View { + ZStack { + // Outer glow ring + Circle() + .stroke( + LinearGradient( + colors: [Color(red: 0, green: 1, blue: 0.82), Color(red: 1, green: 0, blue: 0.8)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: 4 + ) + .frame(width: 140, height: 140) + .blur(radius: 10) + .opacity(pulse ? 0.9 : 0.5) + .rotationEffect(.degrees(rotate ? 360 : 0)) + + // Inner ring + Circle() + .stroke( + LinearGradient( + colors: [Color(red: 0, green: 1, blue: 0.82), Color(red: 1, green: 0, blue: 0.8)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: 3 + ) + .frame(width: 100, height: 100) + .shadow(color: Color(red: 0, green: 1, blue: 0.82).opacity(0.6), radius: pulse ? 20 : 10) + + // Center core + Circle() + .fill(Color.white) + .frame(width: 30, height: 30) + .shadow(color: .white.opacity(0.8), radius: 15) + } + .onAppear { + withAnimation(.linear(duration: 10).repeatForever(autoreverses: false)) { + rotate = true + } + withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) { + pulse = true + } + } + } +} + +// MARK: - Celestial Lock Screen Theme + +struct CelestialLockBackground: View { + @Environment(\.colorScheme) private var colorScheme + @State private var twinkle = false + + var body: some View { + ZStack { + // Deep space gradient + LinearGradient( + colors: colorScheme == .dark ? [ + Color(red: 0.05, green: 0.05, blue: 0.12), + Color(red: 0.08, green: 0.06, blue: 0.15) + ] : [ + Color(red: 0.95, green: 0.94, blue: 0.98), + Color(red: 0.92, green: 0.9, blue: 0.96) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Star field (dark mode only) + if colorScheme == .dark { + Canvas { context, size in + for _ in 0..<50 { + let x = CGFloat.random(in: 0...size.width) + let y = CGFloat.random(in: 0...size.height) + let starSize = CGFloat.random(in: 1...3) + context.fill( + Path(ellipseIn: CGRect(x: x, y: y, width: starSize, height: starSize)), + with: .color(.white.opacity(Double.random(in: 0.3...0.8))) + ) + } + } + .opacity(twinkle ? 0.8 : 1.0) + } + + // Nebula glow + Circle() + .fill(Color(red: 1.0, green: 0.4, blue: 0.5).opacity(colorScheme == .dark ? 0.2 : 0.1)) + .frame(width: 300, height: 300) + .blur(radius: 80) + .offset(y: -50) + + Circle() + .fill(Color(red: 0.6, green: 0.4, blue: 0.9).opacity(colorScheme == .dark ? 0.15 : 0.08)) + .frame(width: 250, height: 250) + .blur(radius: 60) + .offset(x: 80, y: 100) + } + .ignoresSafeArea() + .onAppear { + withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) { + twinkle = true + } + } + } +} + +struct CelestialOrbsElement: View { + @Environment(\.colorScheme) private var colorScheme + @State private var float = false + @State private var rotate = false + + private let orbColors: [Color] = [ + Color(red: 1.0, green: 0.8, blue: 0.3), // Gold + Color(red: 1.0, green: 0.5, blue: 0.5), // Coral + Color(red: 0.6, green: 0.5, blue: 0.9) // Lavender + ] + + var body: some View { + ZStack { + // Orbit ring + Circle() + .stroke(Color.white.opacity(colorScheme == .dark ? 0.15 : 0.2), lineWidth: 1) + .frame(width: 140, height: 140) + .rotationEffect(.degrees(rotate ? 360 : 0)) + + // Orbiting orbs + ForEach(0..<3, id: \.self) { i in + Circle() + .fill( + RadialGradient( + colors: [orbColors[i], orbColors[i].opacity(0.6)], + center: .center, + startRadius: 0, + endRadius: 20 + ) + ) + .frame(width: 28, height: 28) + .shadow(color: orbColors[i].opacity(0.6), radius: 10) + .offset(y: -70) + .rotationEffect(.degrees(Double(i) * 120 + (rotate ? 360 : 0))) + } + + // Center star + Image(systemName: "sparkle") + .font(.system(size: 40, weight: .light)) + .foregroundStyle( + LinearGradient( + colors: [Color(red: 1.0, green: 0.9, blue: 0.7), Color(red: 1.0, green: 0.7, blue: 0.6)], + startPoint: .top, + endPoint: .bottom + ) + ) + .shadow(color: Color(red: 1.0, green: 0.8, blue: 0.5).opacity(0.5), radius: 15) + .scaleEffect(float ? 1.1 : 0.95) + } + .onAppear { + withAnimation(.linear(duration: 20).repeatForever(autoreverses: false)) { + rotate = true + } + withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) { + float = true + } + } + } +} + +// MARK: - Editorial Lock Screen Theme + +struct EditorialLockBackground: View { + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + ZStack { + // Solid elegant background + Color(colorScheme == .dark ? .black : .white) + + // Subtle texture lines + VStack(spacing: 0) { + ForEach(0..<20, id: \.self) { _ in + Rectangle() + .fill(Color.primary.opacity(0.02)) + .frame(height: 1) + Spacer() + } + } + .padding(.horizontal, 40) + } + .ignoresSafeArea() + } +} + +struct EditorialFrameElement: View { + @Environment(\.colorScheme) private var colorScheme + @State private var appear = false + + var body: some View { + ZStack { + // Elegant frame + RoundedRectangle(cornerRadius: 2) + .stroke(Color.primary.opacity(0.3), lineWidth: 1) + .frame(width: 120, height: 150) + + // Inner accent line + VStack { + Rectangle() + .fill(Color.primary) + .frame(width: 40, height: 2) + Spacer() + Rectangle() + .fill(Color.primary) + .frame(width: 40, height: 2) + } + .frame(height: 130) + .opacity(appear ? 1 : 0) + + // Center diamond + Image(systemName: "diamond") + .font(.system(size: 28, weight: .ultraLight)) + .foregroundColor(.primary) + .opacity(appear ? 1 : 0.5) + .scaleEffect(appear ? 1 : 0.9) + } + .onAppear { + withAnimation(.easeOut(duration: 1)) { + appear = true + } + } + } +} + +// MARK: - Mixtape Lock Screen Theme + +struct MixtapeLockBackground: View { + @State private var shift = false + + var body: some View { + ZStack { + // Warm retro gradient + LinearGradient( + colors: [ + Color(red: 0.95, green: 0.45, blue: 0.35), + Color(red: 0.95, green: 0.65, blue: 0.25) + ], + startPoint: shift ? .topLeading : .topTrailing, + endPoint: shift ? .bottomTrailing : .bottomLeading + ) + + // Noise texture + Rectangle() + .fill(.white.opacity(0.03)) + .background( + Canvas { context, size in + for _ in 0..<500 { + let x = CGFloat.random(in: 0...size.width) + let y = CGFloat.random(in: 0...size.height) + context.fill( + Path(ellipseIn: CGRect(x: x, y: y, width: 1, height: 1)), + with: .color(.white.opacity(Double.random(in: 0.02...0.06))) + ) + } + } + ) + } + .ignoresSafeArea() + .onAppear { + withAnimation(.easeInOut(duration: 5).repeatForever(autoreverses: true)) { + shift = true + } + } + } +} + +struct CassetteElement: View { + @State private var spin = false + + var body: some View { + ZStack { + // Cassette body + RoundedRectangle(cornerRadius: 8) + .fill(Color.black.opacity(0.85)) + .frame(width: 140, height: 90) + .shadow(color: .black.opacity(0.3), radius: 10) + + // Label area + RoundedRectangle(cornerRadius: 4) + .fill(Color.white.opacity(0.9)) + .frame(width: 100, height: 30) + .offset(y: -15) + + // Reels + HStack(spacing: 40) { + Circle() + .fill(Color.white.opacity(0.8)) + .frame(width: 30, height: 30) + .overlay( + Circle() + .fill(Color.black.opacity(0.6)) + .frame(width: 10, height: 10) + ) + .rotationEffect(.degrees(spin ? 360 : 0)) + + Circle() + .fill(Color.white.opacity(0.8)) + .frame(width: 30, height: 30) + .overlay( + Circle() + .fill(Color.black.opacity(0.6)) + .frame(width: 10, height: 10) + ) + .rotationEffect(.degrees(spin ? 360 : 0)) + } + .offset(y: 18) + } + .onAppear { + withAnimation(.linear(duration: 4).repeatForever(autoreverses: false)) { + spin = true + } + } + } +} + +// MARK: - Bloom Lock Screen Theme + +struct BloomLockBackground: View { + @Environment(\.colorScheme) private var colorScheme + @State private var bloom = false + + var body: some View { + ZStack { + // Garden gradient + LinearGradient( + colors: colorScheme == .dark ? [ + Color(red: 0.05, green: 0.12, blue: 0.08), + Color(red: 0.08, green: 0.18, blue: 0.1) + ] : [ + Color(red: 0.95, green: 0.98, blue: 0.95), + Color(red: 0.9, green: 0.96, blue: 0.92) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Soft glows + Circle() + .fill(Color(red: 0.3, green: 0.7, blue: 0.4).opacity(colorScheme == .dark ? 0.2 : 0.1)) + .frame(width: 300, height: 300) + .blur(radius: 80) + .offset(y: bloom ? -20 : 20) + + Circle() + .fill(Color(red: 1.0, green: 0.6, blue: 0.7).opacity(colorScheme == .dark ? 0.15 : 0.08)) + .frame(width: 200, height: 200) + .blur(radius: 60) + .offset(x: -50, y: bloom ? 80 : 120) + } + .ignoresSafeArea() + .onAppear { + withAnimation(.easeInOut(duration: 6).repeatForever(autoreverses: true)) { + bloom = true + } + } + } +} + +struct FlowerElement: View { + @Environment(\.colorScheme) private var colorScheme + @State private var bloom = false + + var body: some View { + ZStack { + // Petals + ForEach(0..<6, id: \.self) { i in + Ellipse() + .fill( + LinearGradient( + colors: [ + Color(red: 1.0, green: 0.6, blue: 0.7), + Color(red: 1.0, green: 0.5, blue: 0.6) + ], + startPoint: .top, + endPoint: .bottom + ) + ) + .frame(width: 30, height: bloom ? 60 : 45) + .offset(y: bloom ? -45 : -35) + .rotationEffect(.degrees(Double(i) * 60)) + .shadow(color: Color(red: 1.0, green: 0.5, blue: 0.6).opacity(0.3), radius: 8) + } + + // Center + Circle() + .fill( + RadialGradient( + colors: [ + Color(red: 1.0, green: 0.9, blue: 0.6), + Color(red: 1.0, green: 0.85, blue: 0.4) + ], + center: .center, + startRadius: 0, + endRadius: 25 + ) + ) + .frame(width: 40, height: 40) + .shadow(color: Color(red: 1.0, green: 0.9, blue: 0.6).opacity(0.5), radius: 10) + } + .onAppear { + withAnimation(.easeInOut(duration: 4).repeatForever(autoreverses: true)) { + bloom = true + } + } + } +} + +// MARK: - Heartfelt Lock Screen Theme + +struct HeartfeltLockBackground: View { + @Environment(\.colorScheme) private var colorScheme + @State private var pulse = false + + var body: some View { + ZStack { + // Soft pink gradient + LinearGradient( + colors: colorScheme == .dark ? [ + Color(red: 0.15, green: 0.08, blue: 0.1), + Color(red: 0.1, green: 0.05, blue: 0.08) + ] : [ + Color(red: 1.0, green: 0.95, blue: 0.96), + Color(red: 0.98, green: 0.92, blue: 0.94) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Heart glow + Circle() + .fill(Color(red: 0.9, green: 0.45, blue: 0.55).opacity(colorScheme == .dark ? 0.2 : 0.1)) + .frame(width: 300, height: 300) + .blur(radius: 80) + .scaleEffect(pulse ? 1.1 : 0.95) + } + .ignoresSafeArea() + .onAppear { + withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) { + pulse = true + } + } + } +} + +struct HeartElement: View { + @State private var beat = false + + var body: some View { + ZStack { + // Glow + Image(systemName: "heart.fill") + .font(.system(size: 80)) + .foregroundColor(Color(red: 0.9, green: 0.45, blue: 0.55)) + .blur(radius: 20) + .scaleEffect(beat ? 1.15 : 0.9) + + // Main heart + Image(systemName: "heart.fill") + .font(.system(size: 70)) + .foregroundStyle( + LinearGradient( + colors: [ + Color(red: 0.95, green: 0.55, blue: 0.6), + Color(red: 0.85, green: 0.35, blue: 0.45) + ], + startPoint: .top, + endPoint: .bottom + ) + ) + .shadow(color: Color(red: 0.9, green: 0.45, blue: 0.55).opacity(0.4), radius: 15) + .scaleEffect(beat ? 1.08 : 0.95) + } + .onAppear { + withAnimation(.easeInOut(duration: 0.8).repeatForever(autoreverses: true)) { + beat = true + } + } + } +} + +// MARK: - Minimal Lock Screen Theme + +struct MinimalLockBackground: View { + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + ZStack { + // Clean gradient + LinearGradient( + colors: colorScheme == .dark ? [ + Color(red: 0.1, green: 0.1, blue: 0.1), + Color(red: 0.08, green: 0.08, blue: 0.08) + ] : [ + Color(red: 0.98, green: 0.96, blue: 0.94), + Color(red: 0.95, green: 0.93, blue: 0.9) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Subtle warm accent + Circle() + .fill(Color(red: 0.95, green: 0.6, blue: 0.5).opacity(colorScheme == .dark ? 0.08 : 0.05)) + .frame(width: 400, height: 400) + .blur(radius: 100) + } + .ignoresSafeArea() + } +} + +struct MinimalCircleElement: View { + @Environment(\.colorScheme) private var colorScheme + @State private var breathe = false + + var body: some View { + ZStack { + // Outer ring + Circle() + .stroke(Color.primary.opacity(0.15), lineWidth: 1) + .frame(width: breathe ? 130 : 120, height: breathe ? 130 : 120) + + // Middle ring + Circle() + .stroke(Color.primary.opacity(0.25), lineWidth: 1) + .frame(width: breathe ? 90 : 85, height: breathe ? 90 : 85) + + // Inner circle + Circle() + .fill(Color(red: 0.95, green: 0.6, blue: 0.5).opacity(colorScheme == .dark ? 0.6 : 0.4)) + .frame(width: 50, height: 50) + .scaleEffect(breathe ? 1.05 : 0.95) + } + .onAppear { + withAnimation(.easeInOut(duration: 4).repeatForever(autoreverses: true)) { + breathe = true + } + } + } +} + +// MARK: - Luxe Lock Screen Theme + +struct LuxeLockBackground: View { + @State private var shimmer = false + + var body: some View { + ZStack { + // Rich dark background + LinearGradient( + colors: [ + Color(red: 0.1, green: 0.08, blue: 0.06), + Color(red: 0.06, green: 0.04, blue: 0.02) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Gold shimmer + LinearGradient( + colors: [ + .clear, + Color(red: 0.85, green: 0.7, blue: 0.45).opacity(0.1), + .clear + ], + startPoint: shimmer ? .topLeading : .bottomTrailing, + endPoint: shimmer ? .bottomTrailing : .topLeading + ) + } + .ignoresSafeArea() + .onAppear { + withAnimation(.easeInOut(duration: 4).repeatForever(autoreverses: true)) { + shimmer = true + } + } + } +} + +struct DiamondElement: View { + @State private var rotate = false + @State private var shimmer = false + + var body: some View { + ZStack { + // Glow + Image(systemName: "diamond.fill") + .font(.system(size: 70)) + .foregroundColor(Color(red: 0.85, green: 0.7, blue: 0.45)) + .blur(radius: 25) + .opacity(shimmer ? 0.6 : 0.3) + + // Diamond + Image(systemName: "diamond.fill") + .font(.system(size: 60)) + .foregroundStyle( + LinearGradient( + colors: [ + Color(red: 0.95, green: 0.85, blue: 0.6), + Color(red: 0.75, green: 0.6, blue: 0.35), + Color(red: 0.55, green: 0.45, blue: 0.25) + ], + startPoint: shimmer ? .topLeading : .bottomTrailing, + endPoint: shimmer ? .bottomTrailing : .topLeading + ) + ) + .shadow(color: Color(red: 0.85, green: 0.7, blue: 0.45).opacity(0.5), radius: 20) + .rotationEffect(.degrees(rotate ? 5 : -5)) + } + .onAppear { + withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) { + rotate = true + } + withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) { + shimmer = true + } + } + } +} + +// MARK: - Forecast Lock Screen Theme + +struct ForecastLockBackground: View { + @State private var drift = false + + var body: some View { + ZStack { + // Sky gradient + LinearGradient( + colors: [ + Color(red: 0.55, green: 0.75, blue: 0.95), + Color(red: 0.4, green: 0.6, blue: 0.85), + Color(red: 0.3, green: 0.5, blue: 0.75) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Floating clouds + ForEach(0..<4, id: \.self) { i in + Image(systemName: "cloud.fill") + .font(.system(size: CGFloat.random(in: 40...80))) + .foregroundColor(.white.opacity(Double.random(in: 0.3...0.6))) + .offset( + x: CGFloat(i * 100 - 150) + (drift ? 20 : -20), + y: CGFloat(i * 80 - 200) + ) + .blur(radius: CGFloat.random(in: 2...5)) + } + } + .ignoresSafeArea() + .onAppear { + withAnimation(.easeInOut(duration: 8).repeatForever(autoreverses: true)) { + drift = true + } + } + } +} + +struct WeatherElement: View { + @State private var shine = false + + var body: some View { + ZStack { + // Sun rays + ForEach(0..<8, id: \.self) { i in + Rectangle() + .fill(Color(red: 1.0, green: 0.9, blue: 0.5)) + .frame(width: 3, height: 30) + .offset(y: -60) + .rotationEffect(.degrees(Double(i) * 45)) + .opacity(shine ? 0.8 : 0.4) + } + + // Sun glow + Circle() + .fill(Color(red: 1.0, green: 0.9, blue: 0.5).opacity(0.4)) + .frame(width: 100, height: 100) + .blur(radius: 25) + + // Sun + Circle() + .fill( + RadialGradient( + colors: [ + Color(red: 1.0, green: 0.95, blue: 0.7), + Color(red: 1.0, green: 0.85, blue: 0.4) + ], + center: .center, + startRadius: 0, + endRadius: 35 + ) + ) + .frame(width: 70, height: 70) + .shadow(color: Color(red: 1.0, green: 0.85, blue: 0.4).opacity(0.6), radius: 20) + + // Cloud accent + Image(systemName: "cloud.fill") + .font(.system(size: 35)) + .foregroundColor(.white) + .offset(x: 40, y: 25) + .shadow(color: .black.opacity(0.1), radius: 5) + } + .onAppear { + withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) { + shine = true + } + } + } +} + +// MARK: - Playful Lock Screen Theme + +struct PlayfulLockBackground: View { + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + ZStack { + // Warm cream gradient + LinearGradient( + colors: colorScheme == .dark ? [ + Color(red: 0.15, green: 0.12, blue: 0.1), + Color(red: 0.1, green: 0.08, blue: 0.06) + ] : [ + Color(red: 1.0, green: 0.98, blue: 0.95), + Color(red: 0.98, green: 0.96, blue: 0.92) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Colorful accents + Circle() + .fill(Color(red: 0.95, green: 0.55, blue: 0.35).opacity(colorScheme == .dark ? 0.15 : 0.08)) + .frame(width: 200, height: 200) + .blur(radius: 60) + .offset(x: -80, y: -150) + + Circle() + .fill(Color(red: 0.95, green: 0.75, blue: 0.35).opacity(colorScheme == .dark ? 0.12 : 0.06)) + .frame(width: 180, height: 180) + .blur(radius: 50) + .offset(x: 100, y: 150) + } + .ignoresSafeArea() + } +} + +struct PlayfulEmojiElement: View { + @State private var bounce = false + @State private var wiggle = false + + var body: some View { + ZStack { + // Background circle + Circle() + .fill(Color(red: 0.95, green: 0.55, blue: 0.35).opacity(0.15)) + .frame(width: 140, height: 140) + .scaleEffect(bounce ? 1.05 : 0.98) + + // Main emoji + Text("๐Ÿ˜Š") + .font(.system(size: 80)) + .rotationEffect(.degrees(wiggle ? 8 : -8)) + .scaleEffect(bounce ? 1.1 : 0.95) + } + .onAppear { + withAnimation(.easeInOut(duration: 0.6).repeatForever(autoreverses: true)) { + wiggle = true + } + withAnimation(.easeInOut(duration: 1.2).repeatForever(autoreverses: true)) { + bounce = true + } + } + } +} + +// MARK: - Journal Lock Screen Theme + +struct JournalLockBackground: View { + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + ZStack { + // Warm paper background + Color(colorScheme == .dark ? + Color(red: 0.12, green: 0.1, blue: 0.08) : + Color(red: 0.95, green: 0.92, blue: 0.88) + ) + + // Paper texture lines + VStack(spacing: 28) { + ForEach(0..<25, id: \.self) { _ in + Rectangle() + .fill(Color.primary.opacity(colorScheme == .dark ? 0.08 : 0.06)) + .frame(height: 1) + } + } + .padding(.horizontal, 50) + + // Margin line + Rectangle() + .fill(Color(red: 0.85, green: 0.55, blue: 0.55).opacity(colorScheme == .dark ? 0.3 : 0.2)) + .frame(width: 1) + .offset(x: -120) + } + .ignoresSafeArea() + } +} + +struct JournalBookElement: View { + @Environment(\.colorScheme) private var colorScheme + @State private var open = false + + var body: some View { + ZStack { + // Book shadow + RoundedRectangle(cornerRadius: 8) + .fill(Color.black.opacity(0.2)) + .frame(width: 110, height: 140) + .offset(x: 5, y: 5) + .blur(radius: 8) + + // Book cover + RoundedRectangle(cornerRadius: 8) + .fill( + LinearGradient( + colors: [ + Color(red: 0.55, green: 0.45, blue: 0.35), + Color(red: 0.45, green: 0.35, blue: 0.28) + ], + startPoint: .top, + endPoint: .bottom + ) + ) + .frame(width: 100, height: 130) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(red: 0.65, green: 0.55, blue: 0.45), lineWidth: 2) + ) + + // Spine + Rectangle() + .fill(Color(red: 0.4, green: 0.32, blue: 0.25)) + .frame(width: 8, height: 130) + .offset(x: -46) + + // Title area + VStack(spacing: 8) { + Rectangle() + .fill(Color(red: 0.95, green: 0.9, blue: 0.82)) + .frame(width: 60, height: 30) + .cornerRadius(2) + + Image(systemName: "heart.fill") + .font(.system(size: 20)) + .foregroundColor(Color(red: 0.85, green: 0.55, blue: 0.55)) + } + .scaleEffect(open ? 1.02 : 1.0) + } + .onAppear { + withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) { + open = true + } + } + } +} + // MARK: - Glassmorphic Button struct GlassButton: View { @@ -406,25 +1440,158 @@ struct LockScreenView: View { @State private var showError = false @State private var showContent = false + @AppStorage(UserDefaultsStore.Keys.lockScreenStyle.rawValue, store: GroupUserDefaults.groupDefaults) + private var lockScreenStyleRaw: Int = 0 + + private var lockScreenStyle: LockScreenStyle { + LockScreenStyle(rawValue: lockScreenStyleRaw) ?? .aurora + } + private var isDark: Bool { colorScheme == .dark } - private var primaryText: Color { isDark ? .white : .primary } - private var secondaryText: Color { isDark ? .white.opacity(0.7) : .secondary } - private var tertiaryText: Color { isDark ? .white.opacity(0.5) : .secondary.opacity(0.8) } + + // Style-dependent properties + private var primaryTextColor: Color { + switch lockScreenStyle { + case .neon: + return Color(red: 0, green: 1, blue: 0.82) + case .editorial: + return isDark ? .white : .black + case .mixtape: + return .white + case .luxe: + return Color(red: 0.95, green: 0.85, blue: 0.6) + case .forecast: + return .white + default: + return isDark ? .white : .primary + } + } + + private var secondaryTextColor: Color { + switch lockScreenStyle { + case .neon: + return Color(red: 1, green: 0, blue: 0.8) + case .editorial: + return isDark ? .white.opacity(0.7) : .black.opacity(0.7) + case .mixtape: + return .white.opacity(0.9) + case .luxe: + return Color(red: 0.75, green: 0.6, blue: 0.35) + case .forecast: + return .white.opacity(0.9) + default: + return isDark ? .white.opacity(0.7) : .secondary + } + } + + private var tertiaryTextColor: Color { + switch lockScreenStyle { + case .neon: + return Color.white.opacity(0.6) + case .editorial: + return isDark ? .white.opacity(0.5) : .black.opacity(0.5) + case .mixtape: + return .white.opacity(0.7) + case .luxe: + return Color(red: 0.65, green: 0.55, blue: 0.4).opacity(0.8) + case .forecast: + return .white.opacity(0.7) + default: + return isDark ? .white.opacity(0.5) : .secondary.opacity(0.8) + } + } + + private var titleFont: Font { + switch lockScreenStyle { + case .neon: + return .system(size: 28, weight: .black, design: .monospaced) + case .editorial: + return .system(size: 30, weight: .ultraLight, design: .serif) + case .mixtape: + return .system(size: 28, weight: .black, design: .rounded) + case .zen: + return .system(size: 30, weight: .thin, design: .serif) + case .luxe: + return .system(size: 28, weight: .light, design: .serif) + case .playful: + return .system(size: 28, weight: .bold, design: .rounded) + case .journal: + return .system(size: 26, weight: .medium, design: .serif) + default: + return .system(size: 32, weight: .light, design: .serif) + } + } + + private var titleText: String { + switch lockScreenStyle { + case .neon: return "UNLOCK YOUR" + case .editorial: return "Your Story" + case .mixtape: return "PRESS PLAY" + case .zen: return "Find Your" + case .heartfelt: return "Feel With" + case .luxe: return "Your Sanctuary" + case .forecast: return "Your Forecast" + case .playful: return "Hey There!" + case .journal: return "Your Journal" + case .bloom: return "Time to" + case .celestial: return "Your Feelings" + case .minimal: return "Simply" + default: return "Your Feelings" + } + } + + private var subtitleText: String { + switch lockScreenStyle { + case .neon: return "FULL SIGNAL" + case .editorial: return "Awaits" + case .mixtape: return "ON YOUR MOODS" + case .zen: return "Inner Peace" + case .heartfelt: return "All Your Heart" + case .luxe: return "Awaits" + case .forecast: return "Is Ready" + case .playful: return "Let's Check In!" + case .journal: return "Is Private" + case .bloom: return "Bloom" + case .celestial: return "are safe here" + case .minimal: return "Know Yourself" + default: return "are safe here" + } + } + + private var taglineText: String { + switch lockScreenStyle { + case .neon: return "Authenticate to sync your vibes" + case .editorial: return "Authenticate to continue" + case .mixtape: return "Authenticate to spin your tracks" + case .zen: return "Authenticate to begin your practice" + case .heartfelt: return "Authenticate to open your heart" + case .luxe: return "Authenticate for exclusive access" + case .forecast: return "Authenticate to check the weather" + case .playful: return "Authenticate to start the fun!" + case .journal: return "Authenticate to continue writing" + case .bloom: return "Authenticate to tend your garden" + case .celestial: return "Authenticate to explore the cosmos" + case .minimal: return "Authenticate to continue" + default: return "Authenticate to continue your journey" + } + } var body: some View { ZStack { - // Aurora background - AuroraBackground() + // Themed background + backgroundView - // Floating particles - FloatingParticlesView() + // Floating particles (Aurora only) + if lockScreenStyle == .aurora { + FloatingParticlesView() + } // Main content VStack(spacing: 0) { Spacer() - // Breathing orb - BreathingOrb() + // Central element + centralElement .opacity(showContent ? 1 : 0) .scaleEffect(showContent ? 1 : 0.8) @@ -433,45 +1600,38 @@ struct LockScreenView: View { // Text content VStack(spacing: 12) { - Text("Your Feelings") - .font(.system(size: 32, weight: .light, design: .serif)) - .foregroundColor(primaryText) + Text(titleText) + .font(titleFont) + .foregroundColor(primaryTextColor) + .tracking(lockScreenStyle == .neon || lockScreenStyle == .mixtape ? 2 : 0) - Text("are safe here") - .font(.system(size: 32, weight: .ultraLight, design: .serif)) - .foregroundColor(secondaryText) + Text(subtitleText) + .font(lockScreenStyle == .neon ? .system(size: 24, weight: .bold, design: .monospaced) : titleFont) + .foregroundColor(secondaryTextColor) + .tracking(lockScreenStyle == .neon || lockScreenStyle == .mixtape ? 2 : 0) } + .multilineTextAlignment(.center) .opacity(showContent ? 1 : 0) .offset(y: showContent ? 0 : 20) Spacer() .frame(height: 16) - Text("Authenticate to continue your journey") + Text(taglineText) .font(.system(size: 14, weight: .regular, design: .rounded)) - .foregroundColor(tertiaryText) + .foregroundColor(tertiaryTextColor) .opacity(showContent ? 1 : 0) Spacer() // Unlock button - GlassButton( - icon: authManager.biometricIcon, - title: "Unlock with \(authManager.biometricName)" - ) { - Task { - let success = await authManager.authenticate() - if !success { - showError = true - } - } - } - .disabled(authManager.isAuthenticating) - .opacity(showContent ? 1 : 0) - .offset(y: showContent ? 0 : 30) - .padding(.horizontal, 32) + themedButton + .disabled(authManager.isAuthenticating) + .opacity(showContent ? 1 : 0) + .offset(y: showContent ? 0 : 30) + .padding(.horizontal, 32) - // Passcode button - tappable to authenticate with passcode + // Passcode button if authManager.canUseDevicePasscode { Button { Task { @@ -483,7 +1643,7 @@ struct LockScreenView: View { } label: { Text("Or use your device passcode") .font(.system(size: 13, weight: .medium, design: .rounded)) - .foregroundColor(isDark ? .white.opacity(0.5) : .accentColor) + .foregroundColor(passcodeButtonColor) } .disabled(authManager.isAuthenticating) .padding(.top, 16) @@ -506,12 +1666,10 @@ struct LockScreenView: View { Text("Unable to verify your identity. Please try again.") } .onAppear { - // Animate content in withAnimation(.easeOut(duration: 0.8).delay(0.2)) { showContent = true } - // Auto-trigger authentication if !authManager.isUnlocked && !authManager.isAuthenticating { Task { try? await Task.sleep(for: .milliseconds(800)) @@ -520,16 +1678,292 @@ struct LockScreenView: View { } } } + + // MARK: - Themed Components + + @ViewBuilder + private var backgroundView: some View { + switch lockScreenStyle { + case .aurora: + AuroraBackground() + case .zen: + ZenLockBackground() + case .neon: + NeonLockBackground() + case .celestial: + CelestialLockBackground() + case .editorial: + EditorialLockBackground() + case .mixtape: + MixtapeLockBackground() + case .bloom: + BloomLockBackground() + case .heartfelt: + HeartfeltLockBackground() + case .minimal: + MinimalLockBackground() + case .luxe: + LuxeLockBackground() + case .forecast: + ForecastLockBackground() + case .playful: + PlayfulLockBackground() + case .journal: + JournalLockBackground() + } + } + + @ViewBuilder + private var centralElement: some View { + switch lockScreenStyle { + case .aurora: + BreathingOrb() + case .zen: + ZenEnsoOrb() + case .neon: + NeonRingOrb() + case .celestial: + CelestialOrbsElement() + case .editorial: + EditorialFrameElement() + case .mixtape: + CassetteElement() + case .bloom: + FlowerElement() + case .heartfelt: + HeartElement() + case .minimal: + MinimalCircleElement() + case .luxe: + DiamondElement() + case .forecast: + WeatherElement() + case .playful: + PlayfulEmojiElement() + case .journal: + JournalBookElement() + } + } + + @ViewBuilder + private var themedButton: some View { + switch lockScreenStyle { + case .neon: + NeonUnlockButton( + icon: authManager.biometricIcon, + title: "Unlock with \(authManager.biometricName)" + ) { + Task { + let success = await authManager.authenticate() + if !success { showError = true } + } + } + case .luxe: + LuxeUnlockButton( + icon: authManager.biometricIcon, + title: "Unlock with \(authManager.biometricName)" + ) { + Task { + let success = await authManager.authenticate() + if !success { showError = true } + } + } + case .mixtape: + MixtapeUnlockButton( + icon: authManager.biometricIcon, + title: "Unlock with \(authManager.biometricName)" + ) { + Task { + let success = await authManager.authenticate() + if !success { showError = true } + } + } + default: + GlassButton( + icon: authManager.biometricIcon, + title: "Unlock with \(authManager.biometricName)" + ) { + Task { + let success = await authManager.authenticate() + if !success { showError = true } + } + } + } + } + + private var passcodeButtonColor: Color { + switch lockScreenStyle { + case .neon: + return Color(red: 0, green: 1, blue: 0.82).opacity(0.7) + case .luxe: + return Color(red: 0.85, green: 0.7, blue: 0.45).opacity(0.7) + case .mixtape, .forecast: + return .white.opacity(0.6) + case .editorial: + return isDark ? .white.opacity(0.5) : .black.opacity(0.5) + default: + return isDark ? .white.opacity(0.5) : .accentColor + } + } } -// MARK: - Preview +// MARK: - Themed Unlock Buttons -#Preview("Lock Screen - Dark") { +struct NeonUnlockButton: View { + let icon: String + let title: String + let action: () -> Void + + @State private var pulse = false + + var body: some View { + Button(action: action) { + HStack(spacing: 14) { + Image(systemName: icon) + .font(.system(size: 20, weight: .bold)) + Text(title) + .font(.system(size: 15, weight: .bold, design: .monospaced)) + } + .foregroundStyle( + LinearGradient( + colors: [Color(red: 0, green: 1, blue: 0.82), Color(red: 1, green: 0, blue: 0.8)], + startPoint: .leading, + endPoint: .trailing + ) + ) + .frame(maxWidth: .infinity) + .padding(.vertical, 18) + .padding(.horizontal, 24) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(Color.black) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke( + LinearGradient( + colors: [Color(red: 0, green: 1, blue: 0.82), Color(red: 1, green: 0, blue: 0.8)], + startPoint: .leading, + endPoint: .trailing + ), + lineWidth: 2 + ) + ) + .shadow(color: Color(red: 0, green: 1, blue: 0.82).opacity(pulse ? 0.5 : 0.2), radius: pulse ? 15 : 8) + ) + } + .buttonStyle(PlainButtonStyle()) + .onAppear { + withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) { + pulse = true + } + } + } +} + +struct LuxeUnlockButton: View { + let icon: String + let title: String + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack(spacing: 14) { + Image(systemName: icon) + .font(.system(size: 20, weight: .medium)) + Text(title) + .font(.system(size: 16, weight: .medium, design: .serif)) + } + .foregroundColor(Color(red: 0.95, green: 0.9, blue: 0.75)) + .frame(maxWidth: .infinity) + .padding(.vertical, 18) + .padding(.horizontal, 24) + .background( + RoundedRectangle(cornerRadius: 14) + .fill( + LinearGradient( + colors: [ + Color(red: 0.55, green: 0.45, blue: 0.25), + Color(red: 0.4, green: 0.32, blue: 0.18) + ], + startPoint: .top, + endPoint: .bottom + ) + ) + .overlay( + RoundedRectangle(cornerRadius: 14) + .stroke( + LinearGradient( + colors: [ + Color(red: 0.85, green: 0.7, blue: 0.45), + Color(red: 0.65, green: 0.52, blue: 0.3) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: 1 + ) + ) + .shadow(color: Color(red: 0.85, green: 0.7, blue: 0.45).opacity(0.3), radius: 10) + ) + } + .buttonStyle(PlainButtonStyle()) + } +} + +struct MixtapeUnlockButton: View { + let icon: String + let title: String + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack(spacing: 14) { + Image(systemName: icon) + .font(.system(size: 20, weight: .bold)) + Text(title) + .font(.system(size: 15, weight: .bold, design: .rounded)) + } + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 18) + .padding(.horizontal, 24) + .background( + RoundedRectangle(cornerRadius: 14) + .fill(Color.black.opacity(0.7)) + .overlay( + RoundedRectangle(cornerRadius: 14) + .stroke(Color.white.opacity(0.3), lineWidth: 1) + ) + ) + } + .buttonStyle(PlainButtonStyle()) + } +} + +// MARK: - Previews + +#Preview("Aurora") { LockScreenView(authManager: BiometricAuthManager()) .preferredColorScheme(.dark) } -#Preview("Lock Screen - Light") { +#Preview("Neon") { LockScreenView(authManager: BiometricAuthManager()) - .preferredColorScheme(.light) + .onAppear { + UserDefaults.standard.set(LockScreenStyle.neon.rawValue, forKey: UserDefaultsStore.Keys.lockScreenStyle.rawValue) + } +} + +#Preview("Zen") { + LockScreenView(authManager: BiometricAuthManager()) + .onAppear { + UserDefaults.standard.set(LockScreenStyle.zen.rawValue, forKey: UserDefaultsStore.Keys.lockScreenStyle.rawValue) + } +} + +#Preview("Luxe") { + LockScreenView(authManager: BiometricAuthManager()) + .onAppear { + UserDefaults.standard.set(LockScreenStyle.luxe.rawValue, forKey: UserDefaultsStore.Keys.lockScreenStyle.rawValue) + } } diff --git a/Shared/Views/SettingsView/PaywallPreviewSettingsView.swift b/Shared/Views/SettingsView/PaywallPreviewSettingsView.swift index f30f73b..39ec507 100644 --- a/Shared/Views/SettingsView/PaywallPreviewSettingsView.swift +++ b/Shared/Views/SettingsView/PaywallPreviewSettingsView.swift @@ -120,6 +120,22 @@ struct PaywallPreviewSettingsView: View { NeonMiniPreview() case .minimal: MinimalMiniPreview() + case .zen: + ZenMiniPreview() + case .editorial: + EditorialMiniPreview() + case .mixtape: + MixtapeMiniPreview() + case .heartfelt: + HeartfeltMiniPreview() + case .luxe: + LuxeMiniPreview() + case .forecast: + ForecastMiniPreview() + case .playful: + PlayfulMiniPreview() + case .journal: + JournalMiniPreview() } } @@ -156,6 +172,22 @@ struct PaywallPreviewSettingsView: View { return [Color(red: 0.0, green: 0.9, blue: 0.7), Color(red: 0.9, green: 0.0, blue: 0.7)] case .minimal: return [Color(red: 0.85, green: 0.6, blue: 0.5), Color(red: 0.7, green: 0.5, blue: 0.45)] + case .zen: + return [Color(red: 0.6, green: 0.7, blue: 0.6), Color(red: 0.5, green: 0.6, blue: 0.55)] + case .editorial: + return [Color(red: 0.15, green: 0.15, blue: 0.15), Color(red: 0.3, green: 0.3, blue: 0.3)] + case .mixtape: + return [Color(red: 0.95, green: 0.45, blue: 0.35), Color(red: 0.95, green: 0.65, blue: 0.25)] + case .heartfelt: + return [Color(red: 0.9, green: 0.45, blue: 0.55), Color(red: 0.95, green: 0.6, blue: 0.65)] + case .luxe: + return [Color(red: 0.75, green: 0.6, blue: 0.35), Color(red: 0.55, green: 0.45, blue: 0.25)] + case .forecast: + return [Color(red: 0.4, green: 0.65, blue: 0.85), Color(red: 0.3, green: 0.5, blue: 0.75)] + case .playful: + return [Color(red: 0.95, green: 0.55, blue: 0.35), Color(red: 0.95, green: 0.75, blue: 0.35)] + case .journal: + return [Color(red: 0.55, green: 0.45, blue: 0.35), Color(red: 0.4, green: 0.35, blue: 0.3)] } } } @@ -218,6 +250,14 @@ struct StyleOptionRow: View { case .garden: return "leaf.fill" case .neon: return "bolt.fill" case .minimal: return "circle.grid.2x2" + case .zen: return "circle" + case .editorial: return "textformat" + case .mixtape: return "opticaldisc.fill" + case .heartfelt: return "heart.fill" + case .luxe: return "diamond.fill" + case .forecast: return "cloud.fill" + case .playful: return "face.smiling.fill" + case .journal: return "book.closed.fill" } } @@ -247,6 +287,54 @@ struct StyleOptionRow: View { startPoint: .topLeading, endPoint: .bottomTrailing ) + case .zen: + return LinearGradient( + colors: [Color(red: 0.6, green: 0.7, blue: 0.6), Color(red: 0.5, green: 0.6, blue: 0.55)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + case .editorial: + return LinearGradient( + colors: [Color(red: 0.15, green: 0.15, blue: 0.15), Color(red: 0.3, green: 0.3, blue: 0.3)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + case .mixtape: + return LinearGradient( + colors: [Color(red: 0.95, green: 0.45, blue: 0.35), Color(red: 0.95, green: 0.65, blue: 0.25)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + case .heartfelt: + return LinearGradient( + colors: [Color(red: 0.9, green: 0.45, blue: 0.55), Color(red: 0.95, green: 0.6, blue: 0.65)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + case .luxe: + return LinearGradient( + colors: [Color(red: 0.75, green: 0.6, blue: 0.35), Color(red: 0.55, green: 0.45, blue: 0.25)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + case .forecast: + return LinearGradient( + colors: [Color(red: 0.4, green: 0.65, blue: 0.85), Color(red: 0.3, green: 0.5, blue: 0.75)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + case .playful: + return LinearGradient( + colors: [Color(red: 0.95, green: 0.55, blue: 0.35), Color(red: 0.95, green: 0.75, blue: 0.35)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + case .journal: + return LinearGradient( + colors: [Color(red: 0.55, green: 0.45, blue: 0.35), Color(red: 0.4, green: 0.35, blue: 0.3)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) } } } @@ -467,6 +555,340 @@ struct MinimalMiniPreview: View { } } +struct ZenMiniPreview: View { + @State private var breathe = false + + var body: some View { + ZStack { + // Warm paper background + LinearGradient( + colors: [ + Color(red: 0.96, green: 0.94, blue: 0.90), + Color(red: 0.92, green: 0.90, blue: 0.86) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Content + VStack(spacing: 16) { + // Enso circle + Circle() + .stroke( + Color(red: 0.3, green: 0.35, blue: 0.3), + style: StrokeStyle(lineWidth: 3, lineCap: .round) + ) + .frame(width: breathe ? 55 : 50, height: breathe ? 55 : 50) + .rotationEffect(.degrees(-30)) + + Text("Find Your\nInner Peace") + .font(.system(size: 18, weight: .light, design: .serif)) + .multilineTextAlignment(.center) + .foregroundColor(Color(red: 0.25, green: 0.25, blue: 0.2)) + } + } + .onAppear { + withAnimation(.easeInOut(duration: 4).repeatForever(autoreverses: true)) { + breathe = true + } + } + } +} + +struct EditorialMiniPreview: View { + var body: some View { + ZStack { + // Deep black background + Color.black + + // Content + VStack(spacing: 16) { + // Simple geometric element + Rectangle() + .fill(Color.white) + .frame(width: 40, height: 2) + + Text("THE ART\nOF FEELING") + .font(.system(size: 16, weight: .bold, design: .serif)) + .tracking(3) + .multilineTextAlignment(.center) + .foregroundColor(.white) + + Rectangle() + .fill(Color.white) + .frame(width: 40, height: 2) + } + } + } +} + +struct MixtapeMiniPreview: View { + @State private var spin = false + + var body: some View { + ZStack { + // Warm gradient background + LinearGradient( + colors: [ + Color(red: 0.95, green: 0.45, blue: 0.35), + Color(red: 0.95, green: 0.65, blue: 0.25) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + + // Content + VStack(spacing: 12) { + // Mini cassette + ZStack { + RoundedRectangle(cornerRadius: 4) + .fill(Color.black.opacity(0.8)) + .frame(width: 50, height: 32) + + HStack(spacing: 10) { + Circle() + .fill(Color.white.opacity(0.9)) + .frame(width: 14, height: 14) + .rotationEffect(.degrees(spin ? 360 : 0)) + Circle() + .fill(Color.white.opacity(0.9)) + .frame(width: 14, height: 14) + .rotationEffect(.degrees(spin ? 360 : 0)) + } + } + + Text("YOUR MOOD\nMIXTAPE") + .font(.system(size: 14, weight: .black)) + .multilineTextAlignment(.center) + .foregroundColor(.white) + } + } + .onAppear { + withAnimation(.linear(duration: 3).repeatForever(autoreverses: false)) { + spin = true + } + } + } +} + +struct HeartfeltMiniPreview: View { + @State private var beat = false + + var body: some View { + ZStack { + // Soft pink gradient + LinearGradient( + colors: [ + Color(red: 1.0, green: 0.95, blue: 0.95), + Color(red: 0.98, green: 0.9, blue: 0.92) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Content + VStack(spacing: 12) { + // Floating hearts + HStack(spacing: -8) { + ForEach(0..<3, id: \.self) { i in + Image(systemName: "heart.fill") + .font(.system(size: 20 - CGFloat(i * 4))) + .foregroundColor(Color(red: 0.9, green: 0.45, blue: 0.55)) + .scaleEffect(beat ? 1.1 : 0.95) + } + } + + Text("Feel With\nAll Your Heart") + .font(.system(size: 17, weight: .medium, design: .serif)) + .italic() + .multilineTextAlignment(.center) + .foregroundColor(Color(red: 0.4, green: 0.25, blue: 0.3)) + } + } + .onAppear { + withAnimation(.easeInOut(duration: 0.8).repeatForever(autoreverses: true)) { + beat = true + } + } + } +} + +struct LuxeMiniPreview: View { + @State private var shimmer = false + + var body: some View { + ZStack { + // Deep rich background + LinearGradient( + colors: [ + Color(red: 0.12, green: 0.1, blue: 0.08), + Color(red: 0.08, green: 0.06, blue: 0.04) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Content + VStack(spacing: 14) { + // Diamond icon + Image(systemName: "diamond.fill") + .font(.system(size: 36)) + .foregroundStyle( + LinearGradient( + colors: [ + Color(red: 0.85, green: 0.7, blue: 0.45), + Color(red: 0.65, green: 0.5, blue: 0.3) + ], + startPoint: shimmer ? .topLeading : .bottomTrailing, + endPoint: shimmer ? .bottomTrailing : .topLeading + ) + ) + + Text("Elevate Your\nEmotional Life") + .font(.system(size: 16, weight: .light, design: .serif)) + .tracking(1) + .multilineTextAlignment(.center) + .foregroundColor(Color(red: 0.85, green: 0.8, blue: 0.7)) + } + } + .onAppear { + withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) { + shimmer = true + } + } + } +} + +struct ForecastMiniPreview: View { + @State private var drift = false + + var body: some View { + ZStack { + // Sky gradient + LinearGradient( + colors: [ + Color(red: 0.55, green: 0.75, blue: 0.95), + Color(red: 0.4, green: 0.6, blue: 0.85) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Floating clouds + HStack(spacing: 20) { + Image(systemName: "cloud.fill") + .font(.system(size: 28)) + .foregroundColor(.white.opacity(0.8)) + .offset(x: drift ? 5 : -5) + + Image(systemName: "sun.max.fill") + .font(.system(size: 24)) + .foregroundColor(Color(red: 1.0, green: 0.85, blue: 0.4)) + } + .offset(y: -30) + + // Content + VStack(spacing: 8) { + Text("Your Emotional\nForecast") + .font(.system(size: 17, weight: .semibold, design: .rounded)) + .multilineTextAlignment(.center) + .foregroundColor(.white) + } + .offset(y: 30) + } + .onAppear { + withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) { + drift = true + } + } + } +} + +struct PlayfulMiniPreview: View { + @State private var bounce = false + + var body: some View { + ZStack { + // Warm playful gradient + LinearGradient( + colors: [ + Color(red: 1.0, green: 0.98, blue: 0.94), + Color(red: 0.98, green: 0.95, blue: 0.9) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Bouncing emojis + HStack(spacing: 8) { + Text("๐Ÿ˜Š") + .font(.system(size: 28)) + .offset(y: bounce ? -8 : 0) + Text("๐ŸŽ‰") + .font(.system(size: 24)) + .offset(y: bounce ? 0 : -8) + Text("โœจ") + .font(.system(size: 20)) + .offset(y: bounce ? -8 : 0) + } + .offset(y: -30) + + // Content + VStack(spacing: 8) { + Text("Make Tracking\nFun Again!") + .font(.system(size: 17, weight: .bold, design: .rounded)) + .multilineTextAlignment(.center) + .foregroundColor(Color(red: 0.3, green: 0.25, blue: 0.2)) + } + .offset(y: 35) + } + .onAppear { + withAnimation(.easeInOut(duration: 0.6).repeatForever(autoreverses: true)) { + bounce = true + } + } + } +} + +struct JournalMiniPreview: View { + var body: some View { + ZStack { + // Paper texture background + LinearGradient( + colors: [ + Color(red: 0.95, green: 0.92, blue: 0.88), + Color(red: 0.92, green: 0.88, blue: 0.82) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Horizontal lines like notebook paper + VStack(spacing: 18) { + ForEach(0..<6, id: \.self) { _ in + Rectangle() + .fill(Color(red: 0.7, green: 0.65, blue: 0.6).opacity(0.3)) + .frame(height: 1) + } + } + .padding(.horizontal, 30) + + // Content + VStack(spacing: 12) { + Image(systemName: "book.closed.fill") + .font(.system(size: 32)) + .foregroundColor(Color(red: 0.5, green: 0.4, blue: 0.35)) + + Text("Write Your\nEmotional Story") + .font(.system(size: 16, weight: .medium, design: .serif)) + .italic() + .multilineTextAlignment(.center) + .foregroundColor(Color(red: 0.35, green: 0.3, blue: 0.25)) + } + } + } +} + #Preview { NavigationStack { PaywallPreviewSettingsView()