Add 12 cohesive app themes with matching subscription and lock screens
- Create AppTheme enum bundling color tint, icon pack, entry style, voting layout, paywall style, and lock screen style into unified themes - Add AppThemePickerView for selecting themes with preview cards and detail sheets - Extend PaywallStyle to 12 variants (celestial, garden, neon, minimal, zen, editorial, mixtape, heartfelt, luxe, forecast, playful, journal) - Add LockScreenStyle enum with 13 variants including aurora default - Create themed subscription paywalls with unique backgrounds, decorative elements, and typography for each style - Create themed lock screens with unique backgrounds, central elements, and unlock buttons - Update FeelsSubscriptionStoreView to read style from AppStorage so it auto-matches current theme - Update PaywallPreviewSettingsView to support all 12 paywall styles 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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) {
|
||||
|
||||
414
Shared/Views/CustomizeView/SubViews/AppThemePickerView.swift
Normal file
414
Shared/Views/CustomizeView/SubViews/AppThemePickerView.swift
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user