Add Neon/Synthwave style and 4 paywall themes
- Add 4 distinct paywall themes (Celestial, Garden, Neon, Minimal) with preview/switcher in debug settings - Add Neon voting layout with synthwave equalizer bar design - Upgrade Neon entry style with grid background, cyan/magenta gradients, scanline effects, and mini equalizer visualization - Add PaywallPreviewSettingsView for testing different paywall styles - Use consistent synthwave color palette (cyan #00FFD0, magenta #FF00CC) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1133,6 +1133,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Amplify your emotional intelligence.\nGo premium. Go limitless." : {
|
||||
"comment" : "A description of the premium subscription experience, emphasizing its benefits.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Animation Lab" : {
|
||||
|
||||
},
|
||||
@@ -1783,6 +1787,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Clarity through simplicity.\nPremium unlocks understanding." : {
|
||||
"comment" : "A description of the benefits of the premium subscription.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Clear DB" : {
|
||||
"comment" : "A button label that clears the app's database.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@@ -4666,6 +4674,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Every feeling is a seed.\nPremium helps you grow." : {
|
||||
"comment" : "A description of the premium subscription's benefits.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Exit" : {
|
||||
"comment" : "A button label that dismisses the current view.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@@ -5482,6 +5494,7 @@
|
||||
},
|
||||
"Get unlimited access to all features" : {
|
||||
"comment" : "A description of the benefits of purchasing the premium version of the app.",
|
||||
"extractionState" : "stale",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
@@ -5916,6 +5929,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Join 50,000+ on their journey" : {
|
||||
"comment" : "A description of the social proof badge.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Journal Note" : {
|
||||
"comment" : "The title of the view that appears in the navigation bar.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@@ -8657,6 +8674,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Paywall Styles" : {
|
||||
|
||||
},
|
||||
"Paywall Theme Lab" : {
|
||||
|
||||
},
|
||||
"Personalize Your Experience" : {
|
||||
"comment" : "A title for a tip that encourages users to customize their mood tracking experience.",
|
||||
@@ -8996,6 +9019,16 @@
|
||||
"comment" : "A description of a premium feature that requires a subscription.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Preview" : {
|
||||
|
||||
},
|
||||
"Preview and test different subscription paywall designs" : {
|
||||
|
||||
},
|
||||
"Preview subscription themes" : {
|
||||
"comment" : "A description of the paywall preview feature.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Privacy Lock" : {
|
||||
"comment" : "A title for a toggle that controls whether or not biometric authentication is enabled.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@@ -10815,6 +10848,9 @@
|
||||
"See Your Year at a Glance" : {
|
||||
"comment" : "A title for a feature that lets users see their year's emotional trends.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Select Style" : {
|
||||
|
||||
},
|
||||
"Select this mood" : {
|
||||
"comment" : "A hint that appears when a user taps on a mood button.",
|
||||
@@ -11598,6 +11634,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Simply\nKnow Yourself" : {
|
||||
"comment" : "The title of the first section in the Minimal theme marketing content.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Skip subscription and complete setup" : {
|
||||
"comment" : "A button label that says \"Skip subscription and complete setup\". It's used in the \"OnboardingSubscription\" view.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@@ -13085,6 +13125,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Understand\nYourself Deeper" : {
|
||||
"comment" : "A headline in the premium subscription marketing content.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Unlock AI-Powered Insights" : {
|
||||
"comment" : "A title for a button that allows users to unlock premium insights.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@@ -13262,6 +13306,10 @@
|
||||
"comment" : "A button label that appears when the user is not a premium subscriber, encouraging them to subscribe to unlock more features.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"UNLOCK YOUR\nFULL SIGNAL" : {
|
||||
"comment" : "A title displayed in the neon marketing content view.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Use Siri to Log Moods" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
@@ -13343,6 +13391,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"View Full Paywall" : {
|
||||
|
||||
},
|
||||
"View the app introduction again" : {
|
||||
"comment" : "A button that allows a user to view the app's introductory screen again.",
|
||||
@@ -13605,6 +13656,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Watch Yourself\nBloom" : {
|
||||
"comment" : "A title describing the premium subscription experience.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Wednesday - 10th" : {
|
||||
"comment" : "A label displayed above the date of a mood entry.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@@ -14235,6 +14290,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"isCommentAutoGenerated" : true
|
||||
|
||||
@@ -14,6 +14,7 @@ enum VotingLayoutStyle: Int, CaseIterable {
|
||||
case stacked = 3 // Full-width vertical list
|
||||
case aura = 4 // Atmospheric glowing orbs with flowing layout
|
||||
case orbit = 5 // Celestial orbit with center core
|
||||
case neon = 6 // Synthwave arcade equalizer with glowing segments
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
@@ -23,6 +24,32 @@ enum VotingLayoutStyle: Int, CaseIterable {
|
||||
case .stacked: return "Stacked"
|
||||
case .aura: return "Aura"
|
||||
case .orbit: return "Orbit"
|
||||
case .neon: return "Neon"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PaywallStyle: Int, CaseIterable {
|
||||
case celestial = 0 // Celestial Self-Discovery - aurora, floating orbs
|
||||
case garden = 1 // Garden Growth - organic, blooming nature
|
||||
case neon = 2 // Neon Pulse - synthwave, energetic
|
||||
case minimal = 3 // Minimal Zen - clean, sophisticated
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .celestial: return "Celestial"
|
||||
case .garden: return "Garden"
|
||||
case .neon: return "Neon"
|
||||
case .minimal: return "Minimal"
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .celestial: return "Aurora lights & floating emotion orbs"
|
||||
case .garden: return "Blooming flowers & organic growth"
|
||||
case .neon: return "Synthwave energy & glowing pulses"
|
||||
case .minimal: return "Clean typography & subtle elegance"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,6 +132,7 @@ class UserDefaultsStore {
|
||||
case privacyLockEnabled
|
||||
case healthKitEnabled
|
||||
case healthKitSyncEnabled
|
||||
case paywallStyle
|
||||
|
||||
case contentViewCurrentSelectedHeaderViewBackDays
|
||||
case contentViewHeaderTag
|
||||
|
||||
@@ -84,6 +84,8 @@ struct AddMoodHeaderView: View {
|
||||
AuraVotingView(moodTint: moodTint, onMoodSelected: addItem)
|
||||
case .orbit:
|
||||
OrbitVotingView(moodTint: moodTint, onMoodSelected: addItem)
|
||||
case .neon:
|
||||
NeonVotingView(moodTint: moodTint, onMoodSelected: addItem)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,3 +551,227 @@ struct AddMoodHeaderView_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Layout 7: Neon (Synthwave Arcade Equalizer)
|
||||
|
||||
struct NeonVotingView: View {
|
||||
let moodTint: MoodTints
|
||||
let onMoodSelected: (Mood) -> Void
|
||||
|
||||
@State private var pulsePhase = false
|
||||
@State private var hoveredMood: Mood?
|
||||
|
||||
// Synthwave color palette
|
||||
private let neonCyan = Color(red: 0.0, green: 1.0, blue: 0.82)
|
||||
private let neonMagenta = Color(red: 1.0, green: 0.0, blue: 0.8)
|
||||
private let neonYellow = Color(red: 1.0, green: 0.9, blue: 0.0)
|
||||
private let deepBlack = Color(red: 0.02, green: 0.02, blue: 0.04)
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Grid background
|
||||
neonGridBackground
|
||||
|
||||
// Equalizer bars
|
||||
HStack(spacing: 8) {
|
||||
ForEach(Mood.allValues, id: \.self) { mood in
|
||||
NeonEqualizerBar(
|
||||
mood: mood,
|
||||
moodTint: moodTint,
|
||||
isHovered: hoveredMood == mood,
|
||||
pulsePhase: pulsePhase,
|
||||
neonCyan: neonCyan,
|
||||
neonMagenta: neonMagenta,
|
||||
onTap: { onMoodSelected(mood) }
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 20)
|
||||
}
|
||||
.frame(height: 200)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.stroke(
|
||||
LinearGradient(
|
||||
colors: [neonCyan.opacity(0.6), neonMagenta.opacity(0.6)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
),
|
||||
lineWidth: 1
|
||||
)
|
||||
)
|
||||
.shadow(color: neonCyan.opacity(0.2), radius: 20, x: 0, y: 0)
|
||||
.shadow(color: neonMagenta.opacity(0.15), radius: 30, x: 0, y: 10)
|
||||
.onAppear {
|
||||
withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) {
|
||||
pulsePhase = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var neonGridBackground: some View {
|
||||
ZStack {
|
||||
// Deep black base
|
||||
deepBlack
|
||||
|
||||
// Grid
|
||||
Canvas { context, size in
|
||||
let gridSpacing: CGFloat = 20
|
||||
let cyanColor = Color(red: 0.0, green: 0.8, blue: 0.7)
|
||||
|
||||
// Horizontal lines
|
||||
for y in stride(from: 0, to: size.height, by: gridSpacing) {
|
||||
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(cyanColor.opacity(0.08)), lineWidth: 0.5)
|
||||
}
|
||||
|
||||
// Vertical lines
|
||||
for x in stride(from: 0, to: size.width, by: gridSpacing) {
|
||||
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(cyanColor.opacity(0.08)), lineWidth: 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
// Ambient glow at bottom
|
||||
LinearGradient(
|
||||
colors: [
|
||||
neonMagenta.opacity(0.15),
|
||||
Color.clear
|
||||
],
|
||||
startPoint: .bottom,
|
||||
endPoint: .top
|
||||
)
|
||||
.blur(radius: 30)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NeonEqualizerBar: View {
|
||||
let mood: Mood
|
||||
let moodTint: MoodTints
|
||||
let isHovered: Bool
|
||||
let pulsePhase: Bool
|
||||
let neonCyan: Color
|
||||
let neonMagenta: Color
|
||||
let onTap: () -> Void
|
||||
|
||||
@State private var isPressed = false
|
||||
|
||||
private var barHeight: CGFloat {
|
||||
switch mood {
|
||||
case .great: return 140
|
||||
case .good: return 115
|
||||
case .average: return 90
|
||||
case .bad: return 65
|
||||
case .horrible: return 45
|
||||
default: return 90
|
||||
}
|
||||
}
|
||||
|
||||
private var barColor: Color {
|
||||
switch mood {
|
||||
case .great: return neonCyan
|
||||
case .good: return Color(red: 0.2, green: 1.0, blue: 0.6)
|
||||
case .average: return Color(red: 1.0, green: 0.9, blue: 0.0)
|
||||
case .bad: return Color(red: 1.0, green: 0.5, blue: 0.0)
|
||||
case .horrible: return neonMagenta
|
||||
default: return Color(red: 1.0, green: 0.9, blue: 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: onTap) {
|
||||
VStack(spacing: 8) {
|
||||
// The equalizer bar
|
||||
ZStack(alignment: .bottom) {
|
||||
// Glow background
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(barColor.opacity(pulsePhase ? 0.15 : 0.08))
|
||||
.frame(height: barHeight + 20)
|
||||
.blur(radius: 15)
|
||||
|
||||
// Main bar
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
barColor,
|
||||
barColor.opacity(0.7)
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
)
|
||||
.frame(height: isPressed ? barHeight * 0.9 : barHeight)
|
||||
.shadow(color: barColor.opacity(0.8), radius: pulsePhase ? 12 : 8, x: 0, y: 0)
|
||||
.shadow(color: barColor.opacity(0.4), radius: pulsePhase ? 20 : 15, x: 0, y: 5)
|
||||
|
||||
// Top highlight
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [Color.white.opacity(0.5), Color.clear],
|
||||
startPoint: .top,
|
||||
endPoint: .center
|
||||
)
|
||||
)
|
||||
.frame(height: isPressed ? barHeight * 0.9 : barHeight)
|
||||
|
||||
// Level indicators (horizontal lines)
|
||||
VStack(spacing: 8) {
|
||||
ForEach(0..<Int(barHeight / 15), id: \.self) { _ in
|
||||
Rectangle()
|
||||
.fill(Color.black.opacity(0.3))
|
||||
.frame(height: 2)
|
||||
}
|
||||
}
|
||||
.frame(height: isPressed ? barHeight * 0.9 - 10 : barHeight - 10)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
.padding(.horizontal, 4)
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
.frame(maxHeight: 160, alignment: .bottom)
|
||||
|
||||
// Mood label
|
||||
Text(mood.shortLabel)
|
||||
.font(.system(size: 10, weight: .bold, design: .monospaced))
|
||||
.foregroundColor(barColor)
|
||||
.shadow(color: barColor.opacity(0.8), radius: 4, x: 0, y: 0)
|
||||
}
|
||||
}
|
||||
.buttonStyle(NeonBarButtonStyle(isPressed: $isPressed))
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
struct NeonBarButtonStyle: ButtonStyle {
|
||||
@Binding var isPressed: Bool
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
|
||||
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
|
||||
.onChange(of: configuration.isPressed) { _, newValue in
|
||||
isPressed = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Mood {
|
||||
var shortLabel: String {
|
||||
switch self {
|
||||
case .great: return "GRT"
|
||||
case .good: return "GUD"
|
||||
case .average: return "AVG"
|
||||
case .bad: return "BAD"
|
||||
case .horrible: return "HRB"
|
||||
default: return "---"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,6 +587,27 @@ struct VotingLayoutPickerCompact: View {
|
||||
.offset(orbitOffset(index: index, total: 5, radius: 16))
|
||||
}
|
||||
}
|
||||
case .neon:
|
||||
// Equalizer bars
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(Color.black)
|
||||
.frame(width: 36, height: 36)
|
||||
HStack(spacing: 2) {
|
||||
ForEach(0..<5, id: \.self) { index in
|
||||
let heights: [CGFloat] = [24, 18, 14, 10, 8]
|
||||
RoundedRectangle(cornerRadius: 1)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [Color(red: 0, green: 1, blue: 0.82), Color(red: 1, green: 0, blue: 0.8)],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
)
|
||||
.frame(width: 4, height: heights[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -151,6 +151,30 @@ struct VotingLayoutPickerView: View {
|
||||
.offset(orbitOffset(index: index, total: 5, radius: 16))
|
||||
}
|
||||
}
|
||||
case .neon:
|
||||
// Equalizer bars
|
||||
ZStack {
|
||||
// Grid background hint
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(Color.black)
|
||||
.frame(width: 36, height: 36)
|
||||
|
||||
// Equalizer bars
|
||||
HStack(spacing: 2) {
|
||||
ForEach(0..<5, id: \.self) { index in
|
||||
let heights: [CGFloat] = [24, 18, 14, 10, 8]
|
||||
RoundedRectangle(cornerRadius: 1)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [Color(red: 0, green: 1, blue: 0.82), Color(red: 1, green: 0, blue: 0.8)],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
)
|
||||
.frame(width: 4, height: heights[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -590,103 +590,205 @@ struct EntryListView: View {
|
||||
.background(colorScheme == .dark ? Color(.systemGray6) : .white)
|
||||
}
|
||||
|
||||
// MARK: - Neon Style (Cyberpunk/Synthwave)
|
||||
// MARK: - Neon Style (Synthwave Arcade)
|
||||
private var neonStyle: some View {
|
||||
ZStack {
|
||||
// Dark base with scanline effect
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(Color.black)
|
||||
let neonCyan = Color(red: 0.0, green: 1.0, blue: 0.82)
|
||||
let neonMagenta = Color(red: 1.0, green: 0.0, blue: 0.8)
|
||||
let deepBlack = Color(red: 0.02, green: 0.02, blue: 0.04)
|
||||
|
||||
// Scanline overlay
|
||||
VStack(spacing: 2) {
|
||||
ForEach(0..<20, id: \.self) { _ in
|
||||
Rectangle()
|
||||
.fill(Color.white.opacity(0.03))
|
||||
.frame(height: 1)
|
||||
Spacer().frame(height: 3)
|
||||
// Map mood to synthwave color spectrum
|
||||
let synthwaveColor: Color = {
|
||||
switch entry.mood {
|
||||
case .great: return neonCyan
|
||||
case .good: return Color(red: 0.0, green: 0.9, blue: 0.6) // Cyan-green
|
||||
case .average: return Color(red: 0.9, green: 0.9, blue: 0.2) // Neon yellow
|
||||
case .bad: return Color(red: 1.0, green: 0.5, blue: 0.3) // Neon orange
|
||||
case .horrible: return neonMagenta
|
||||
default: return Color(red: 0.9, green: 0.9, blue: 0.2) // Fallback yellow
|
||||
}
|
||||
}()
|
||||
|
||||
return ZStack {
|
||||
// Deep black base
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(deepBlack)
|
||||
|
||||
// Grid background pattern
|
||||
Canvas { context, size in
|
||||
let gridSpacing: CGFloat = 12
|
||||
let gridColor = neonCyan.opacity(0.08)
|
||||
|
||||
// Horizontal grid lines
|
||||
for y in stride(from: 0, through: size.height, by: gridSpacing) {
|
||||
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(gridColor), lineWidth: 0.5)
|
||||
}
|
||||
|
||||
// Vertical grid lines
|
||||
for x in stride(from: 0, through: size.width, by: gridSpacing) {
|
||||
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(gridColor), lineWidth: 0.5)
|
||||
}
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
|
||||
// Scanline overlay for CRT effect
|
||||
VStack(spacing: 0) {
|
||||
ForEach(0..<30, id: \.self) { _ in
|
||||
Rectangle()
|
||||
.fill(Color.white.opacity(0.015))
|
||||
.frame(height: 1)
|
||||
Spacer().frame(height: 2)
|
||||
}
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
|
||||
// Content
|
||||
HStack(spacing: 16) {
|
||||
// Neon-outlined mood indicator
|
||||
// Neon equalizer-style mood indicator
|
||||
ZStack {
|
||||
// Glow effect
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(isMissing ? Color.gray : moodColor, lineWidth: 2)
|
||||
// Outer glow ring
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(
|
||||
LinearGradient(
|
||||
colors: isMissing
|
||||
? [Color.gray.opacity(0.3)]
|
||||
: [neonCyan.opacity(0.6), neonMagenta.opacity(0.6)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
),
|
||||
lineWidth: 2
|
||||
)
|
||||
.blur(radius: 4)
|
||||
.opacity(0.8)
|
||||
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(isMissing ? Color.gray : moodColor, lineWidth: 2)
|
||||
// Inner border
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(
|
||||
LinearGradient(
|
||||
colors: isMissing
|
||||
? [Color.gray.opacity(0.4)]
|
||||
: [neonCyan, neonMagenta],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
),
|
||||
lineWidth: 1.5
|
||||
)
|
||||
|
||||
// Mood icon with synthwave glow
|
||||
imagePack.icon(forMood: entry.mood)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 28, height: 28)
|
||||
.foregroundColor(isMissing ? .gray : moodColor)
|
||||
.shadow(color: isMissing ? .clear : moodColor, radius: 8, x: 0, y: 0)
|
||||
.foregroundColor(isMissing ? .gray : synthwaveColor)
|
||||
.shadow(color: isMissing ? .clear : synthwaveColor.opacity(0.9), radius: 8, x: 0, y: 0)
|
||||
.shadow(color: isMissing ? .clear : synthwaveColor.opacity(0.5), radius: 16, x: 0, y: 0)
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
}
|
||||
.frame(width: 52, height: 52)
|
||||
.frame(width: 54, height: 54)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
// Date in monospace terminal style
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
// Date in cyan monospace
|
||||
Text(entry.forDate, format: .dateTime.year().month(.twoDigits).day(.twoDigits))
|
||||
.font(.caption.weight(.medium).monospaced())
|
||||
.foregroundColor(Color(red: 0.4, green: 1.0, blue: 0.4)) // Terminal green
|
||||
.font(.system(.caption, design: .monospaced).weight(.semibold))
|
||||
.foregroundColor(neonCyan)
|
||||
.shadow(color: neonCyan.opacity(0.5), radius: 4, x: 0, y: 0)
|
||||
|
||||
if isMissing {
|
||||
Text("NO_DATA")
|
||||
.font(.headline.weight(.bold).monospaced())
|
||||
.foregroundColor(.gray)
|
||||
.font(.system(.headline, design: .monospaced).weight(.black))
|
||||
.foregroundColor(.gray.opacity(0.6))
|
||||
} else {
|
||||
// Mood in glowing text
|
||||
// Mood text with synthwave gradient glow
|
||||
Text(entry.moodString.uppercased())
|
||||
.font(.headline.weight(.black))
|
||||
.foregroundColor(moodColor)
|
||||
.shadow(color: moodColor.opacity(0.8), radius: 6, x: 0, y: 0)
|
||||
.shadow(color: moodColor.opacity(0.4), radius: 12, x: 0, y: 0)
|
||||
.font(.system(.headline, design: .monospaced).weight(.black))
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [neonCyan, synthwaveColor, neonMagenta],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
)
|
||||
.shadow(color: synthwaveColor.opacity(0.8), radius: 6, x: 0, y: 0)
|
||||
.shadow(color: neonMagenta.opacity(0.3), radius: 12, x: 0, y: 0)
|
||||
}
|
||||
|
||||
// Weekday
|
||||
// Weekday in magenta
|
||||
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
||||
.font(.caption2.weight(.medium).monospaced())
|
||||
.foregroundColor(.white.opacity(0.4))
|
||||
.font(.system(.caption2, design: .monospaced).weight(.medium))
|
||||
.foregroundColor(neonMagenta.opacity(0.7))
|
||||
.textCase(.uppercase)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Chevron with glow
|
||||
// Equalizer bars indicator (mini visualization)
|
||||
if !isMissing {
|
||||
HStack(spacing: 2) {
|
||||
ForEach(0..<5, id: \.self) { index in
|
||||
let barHeight: CGFloat = {
|
||||
let moodIndex = Mood.allValues.firstIndex(of: entry.mood) ?? 2
|
||||
let heights: [[CGFloat]] = [
|
||||
[28, 22, 16, 10, 6], // Great
|
||||
[24, 28, 18, 12, 8], // Good
|
||||
[16, 20, 28, 20, 16], // Okay
|
||||
[8, 12, 18, 28, 24], // Bad
|
||||
[6, 10, 16, 22, 28] // Awful
|
||||
]
|
||||
return heights[moodIndex][index]
|
||||
}()
|
||||
|
||||
RoundedRectangle(cornerRadius: 1)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [neonCyan, neonMagenta],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
)
|
||||
.frame(width: 3, height: barHeight)
|
||||
.shadow(color: neonCyan.opacity(0.5), radius: 2, x: 0, y: 0)
|
||||
}
|
||||
}
|
||||
.padding(.trailing, 4)
|
||||
}
|
||||
|
||||
// Chevron with gradient glow
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.subheadline.weight(.bold))
|
||||
.foregroundColor(isMissing ? .gray : moodColor)
|
||||
.shadow(color: isMissing ? .clear : moodColor, radius: 4, x: 0, y: 0)
|
||||
.foregroundStyle(
|
||||
isMissing
|
||||
? AnyShapeStyle(Color.gray.opacity(0.4))
|
||||
: AnyShapeStyle(LinearGradient(
|
||||
colors: [neonCyan, neonMagenta],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
))
|
||||
)
|
||||
.shadow(color: isMissing ? .clear : neonCyan.opacity(0.5), radius: 4, x: 0, y: 0)
|
||||
}
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.vertical, 16)
|
||||
.padding(.vertical, 14)
|
||||
|
||||
// Neon border
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
// Cyan-to-magenta gradient border
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.stroke(
|
||||
LinearGradient(
|
||||
colors: isMissing
|
||||
? [Color.gray.opacity(0.3), Color.gray.opacity(0.1)]
|
||||
: [moodColor.opacity(0.8), moodColor.opacity(0.3)],
|
||||
? [Color.gray.opacity(0.2), Color.gray.opacity(0.1)]
|
||||
: [neonCyan.opacity(0.7), neonMagenta.opacity(0.7)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
),
|
||||
lineWidth: 1
|
||||
)
|
||||
}
|
||||
.shadow(
|
||||
color: isMissing ? .clear : moodColor.opacity(0.3),
|
||||
radius: 12,
|
||||
x: 0,
|
||||
y: 4
|
||||
)
|
||||
// Outer glow effect
|
||||
.shadow(color: isMissing ? .clear : neonCyan.opacity(0.2), radius: 12, x: 0, y: 2)
|
||||
.shadow(color: isMissing ? .clear : neonMagenta.opacity(0.15), radius: 20, x: 0, y: 4)
|
||||
}
|
||||
|
||||
// MARK: - Ink Style (Japanese Zen/Calligraphy)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
476
Shared/Views/SettingsView/PaywallPreviewSettingsView.swift
Normal file
476
Shared/Views/SettingsView/PaywallPreviewSettingsView.swift
Normal file
@@ -0,0 +1,476 @@
|
||||
//
|
||||
// PaywallPreviewSettingsView.swift
|
||||
// Feels
|
||||
//
|
||||
// Debug view for previewing and switching paywall styles.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
#if DEBUG
|
||||
struct PaywallPreviewSettingsView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var selectedStyle: PaywallStyle = .celestial
|
||||
@State private var showFullPreview = false
|
||||
@EnvironmentObject var iapManager: IAPManager
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 24) {
|
||||
headerSection
|
||||
|
||||
stylePicker
|
||||
|
||||
previewCard
|
||||
|
||||
fullPreviewButton
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.background(Color(.systemGroupedBackground))
|
||||
.navigationTitle("Paywall Styles")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showFullPreview) {
|
||||
FeelsSubscriptionStoreView(style: selectedStyle)
|
||||
.environmentObject(iapManager)
|
||||
}
|
||||
}
|
||||
|
||||
private var headerSection: some View {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "paintpalette.fill")
|
||||
.font(.system(size: 40))
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [.purple, .pink, .orange],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
|
||||
Text("Paywall Theme Lab")
|
||||
.font(.title2.bold())
|
||||
|
||||
Text("Preview and test different subscription paywall designs")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding(.vertical)
|
||||
}
|
||||
|
||||
private var stylePicker: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Select Style")
|
||||
.font(.headline)
|
||||
|
||||
ForEach(PaywallStyle.allCases, id: \.self) { style in
|
||||
StyleOptionRow(
|
||||
style: style,
|
||||
isSelected: selectedStyle == style,
|
||||
onTap: { selectedStyle = style }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var previewCard: some View {
|
||||
VStack(spacing: 0) {
|
||||
// Mini preview header
|
||||
HStack {
|
||||
Text("Preview")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
Text(selectedStyle.displayName)
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 10)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
|
||||
// Mini preview content
|
||||
miniPreview
|
||||
.frame(height: 280)
|
||||
.clipped()
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.stroke(Color(.separator), lineWidth: 0.5)
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var miniPreview: some View {
|
||||
switch selectedStyle {
|
||||
case .celestial:
|
||||
CelestialMiniPreview()
|
||||
case .garden:
|
||||
GardenMiniPreview()
|
||||
case .neon:
|
||||
NeonMiniPreview()
|
||||
case .minimal:
|
||||
MinimalMiniPreview()
|
||||
}
|
||||
}
|
||||
|
||||
private var fullPreviewButton: some View {
|
||||
Button {
|
||||
showFullPreview = true
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "arrow.up.left.and.arrow.down.right")
|
||||
Text("View Full Paywall")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: gradientColors,
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
)
|
||||
.foregroundColor(.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
}
|
||||
}
|
||||
|
||||
private var gradientColors: [Color] {
|
||||
switch selectedStyle {
|
||||
case .celestial:
|
||||
return [Color(red: 1.0, green: 0.4, blue: 0.5), Color(red: 0.6, green: 0.4, blue: 0.9)]
|
||||
case .garden:
|
||||
return [Color(red: 0.4, green: 0.75, blue: 0.45), Color(red: 0.3, green: 0.6, blue: 0.4)]
|
||||
case .neon:
|
||||
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)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Style Option Row
|
||||
|
||||
struct StyleOptionRow: View {
|
||||
let style: PaywallStyle
|
||||
let isSelected: Bool
|
||||
let onTap: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: onTap) {
|
||||
HStack(spacing: 14) {
|
||||
// Style icon
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(iconGradient)
|
||||
.frame(width: 44, height: 44)
|
||||
|
||||
Image(systemName: iconName)
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
// Text
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(style.displayName)
|
||||
.font(.body.weight(.medium))
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text(style.description)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Selection indicator
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.title3)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(isSelected ? Color.accentColor : Color.clear, lineWidth: 2)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private var iconName: String {
|
||||
switch style {
|
||||
case .celestial: return "sparkles"
|
||||
case .garden: return "leaf.fill"
|
||||
case .neon: return "bolt.fill"
|
||||
case .minimal: return "circle.grid.2x2"
|
||||
}
|
||||
}
|
||||
|
||||
private var iconGradient: LinearGradient {
|
||||
switch style {
|
||||
case .celestial:
|
||||
return LinearGradient(
|
||||
colors: [Color(red: 1.0, green: 0.4, blue: 0.5), Color(red: 0.6, green: 0.4, blue: 0.9)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
case .garden:
|
||||
return LinearGradient(
|
||||
colors: [Color(red: 0.4, green: 0.75, blue: 0.45), Color(red: 0.3, green: 0.6, blue: 0.4)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
case .neon:
|
||||
return LinearGradient(
|
||||
colors: [Color(red: 0.0, green: 0.9, blue: 0.7), Color(red: 0.9, green: 0.0, blue: 0.7)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
case .minimal:
|
||||
return LinearGradient(
|
||||
colors: [Color(red: 0.85, green: 0.6, blue: 0.5), Color(red: 0.7, green: 0.5, blue: 0.45)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mini Previews
|
||||
|
||||
struct CelestialMiniPreview: View {
|
||||
@State private var animate = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Background
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color(red: 0.05, green: 0.05, blue: 0.12),
|
||||
Color(red: 0.08, green: 0.06, blue: 0.15)
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
|
||||
// Glow
|
||||
Circle()
|
||||
.fill(Color(red: 1.0, green: 0.4, blue: 0.5).opacity(0.3))
|
||||
.frame(width: 200, height: 200)
|
||||
.blur(radius: 60)
|
||||
.offset(y: animate ? -20 : 0)
|
||||
|
||||
// Content
|
||||
VStack(spacing: 12) {
|
||||
// Mini orbs
|
||||
HStack(spacing: -10) {
|
||||
ForEach(0..<3, id: \.self) { i in
|
||||
Circle()
|
||||
.fill(orbColors[i])
|
||||
.frame(width: 24, height: 24)
|
||||
.shadow(color: orbColors[i].opacity(0.5), radius: 8)
|
||||
}
|
||||
}
|
||||
|
||||
Text("Understand\nYourself Deeper")
|
||||
.font(.system(size: 18, weight: .bold, design: .serif))
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
|
||||
animate = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var orbColors: [Color] {
|
||||
[
|
||||
Color(red: 1.0, green: 0.8, blue: 0.3),
|
||||
Color(red: 1.0, green: 0.5, blue: 0.5),
|
||||
Color(red: 0.6, green: 0.5, blue: 0.9)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
struct GardenMiniPreview: View {
|
||||
@State private var bloom = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Background
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color(red: 0.05, green: 0.12, blue: 0.08),
|
||||
Color(red: 0.08, green: 0.18, blue: 0.1)
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
|
||||
// Glow
|
||||
Circle()
|
||||
.fill(Color(red: 0.3, green: 0.7, blue: 0.4).opacity(0.25))
|
||||
.frame(width: 200, height: 200)
|
||||
.blur(radius: 60)
|
||||
|
||||
// Content
|
||||
VStack(spacing: 12) {
|
||||
// Mini flower
|
||||
ZStack {
|
||||
ForEach(0..<6, id: \.self) { i in
|
||||
Ellipse()
|
||||
.fill(Color(red: 1.0, green: 0.6, blue: 0.7))
|
||||
.frame(width: 14, height: bloom ? 28 : 20)
|
||||
.offset(y: bloom ? -22 : -16)
|
||||
.rotationEffect(.degrees(Double(i) * 60))
|
||||
}
|
||||
Circle()
|
||||
.fill(Color(red: 1.0, green: 0.9, blue: 0.6))
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
|
||||
Text("Watch Yourself\nBloom")
|
||||
.font(.system(size: 18, weight: .bold, design: .serif))
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
|
||||
bloom = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NeonMiniPreview: View {
|
||||
@State private var pulse = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Background
|
||||
Color(red: 0.02, green: 0.02, blue: 0.05)
|
||||
|
||||
// Grid
|
||||
Canvas { context, size in
|
||||
let spacing: CGFloat = 20
|
||||
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.1)), lineWidth: 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
// Glows
|
||||
Circle()
|
||||
.fill(Color.cyan.opacity(pulse ? 0.3 : 0.15))
|
||||
.frame(width: 150, height: 150)
|
||||
.blur(radius: 50)
|
||||
.offset(y: -40)
|
||||
|
||||
Circle()
|
||||
.fill(Color.pink.opacity(pulse ? 0.2 : 0.1))
|
||||
.frame(width: 120, height: 120)
|
||||
.blur(radius: 40)
|
||||
.offset(x: 30, y: 40)
|
||||
|
||||
// Content
|
||||
VStack(spacing: 12) {
|
||||
// Neon ring
|
||||
Circle()
|
||||
.stroke(
|
||||
LinearGradient(
|
||||
colors: [.cyan, .pink],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
),
|
||||
lineWidth: 3
|
||||
)
|
||||
.frame(width: 50, height: 50)
|
||||
.shadow(color: .cyan.opacity(0.5), radius: pulse ? 15 : 8)
|
||||
|
||||
Text("UNLOCK YOUR\nFULL SIGNAL")
|
||||
.font(.system(size: 14, weight: .black, design: .monospaced))
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(
|
||||
LinearGradient(colors: [.cyan, .pink], startPoint: .leading, endPoint: .trailing)
|
||||
)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) {
|
||||
pulse = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MinimalMiniPreview: View {
|
||||
@State private var breathe = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Background
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color(red: 0.98, green: 0.96, blue: 0.92),
|
||||
Color(red: 0.95, green: 0.93, blue: 0.88)
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
|
||||
// Content
|
||||
VStack(spacing: 16) {
|
||||
// Breathing circles
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(Color(red: 0.8, green: 0.7, blue: 0.6).opacity(0.3), lineWidth: 1)
|
||||
.frame(width: breathe ? 60 : 50, height: breathe ? 60 : 50)
|
||||
|
||||
Circle()
|
||||
.fill(Color(red: 0.95, green: 0.6, blue: 0.5).opacity(0.4))
|
||||
.frame(width: 30, height: 30)
|
||||
.scaleEffect(breathe ? 1.1 : 0.95)
|
||||
}
|
||||
|
||||
Text("Simply\nKnow Yourself")
|
||||
.font(.system(size: 18, weight: .light, design: .serif))
|
||||
.italic()
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(Color(red: 0.2, green: 0.15, blue: 0.1))
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) {
|
||||
breathe = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
PaywallPreviewSettingsView()
|
||||
.environmentObject(IAPManager())
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -54,6 +54,7 @@ struct SettingsContentView: View {
|
||||
debugSectionHeader
|
||||
trialDateButton
|
||||
animationLabButton
|
||||
paywallPreviewButton
|
||||
#endif
|
||||
|
||||
Spacer()
|
||||
@@ -245,6 +246,7 @@ struct SettingsContentView: View {
|
||||
}
|
||||
|
||||
@State private var showAnimationLab = false
|
||||
@State private var showPaywallPreview = false
|
||||
|
||||
private var animationLabButton: some View {
|
||||
ZStack {
|
||||
@@ -284,6 +286,51 @@ struct SettingsContentView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var paywallPreviewButton: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button {
|
||||
showPaywallPreview = true
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "paintpalette.fill")
|
||||
.font(.title2)
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [.purple, .pink, .orange],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.frame(width: 32)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Paywall Styles")
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text("Preview subscription themes")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
.sheet(isPresented: $showPaywallPreview) {
|
||||
NavigationStack {
|
||||
PaywallPreviewSettingsView()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - Privacy Lock Toggle
|
||||
|
||||
Reference in New Issue
Block a user