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