Move vote animation to Customize tab as persistent user setting

Replace random animation selection with a user-configurable picker on
the Customize tab between Mood Style and Notifications. Confetti is
the default. Selecting a style shows an inline preview that auto-plays
the animation then dismisses itself. Remove Animation Lab from Settings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-24 11:47:40 -06:00
parent c643feb1d6
commit 36be57e47d
6 changed files with 193 additions and 51 deletions

View File

@@ -93,6 +93,11 @@ struct CustomizeContentView: View {
}
}
// VOTE ANIMATION
SettingsSection(title: "Vote Animation") {
CelebrationAnimationPickerCompact()
}
// WIDGETS
// SettingsSection(title: "Widgets") {
// CustomWidgetSection()
@@ -162,6 +167,11 @@ struct CustomizeView: View {
}
}
// VOTE ANIMATION
SettingsSection(title: "Vote Animation") {
CelebrationAnimationPickerCompact()
}
// WIDGETS
// SettingsSection(title: "Widgets") {
// CustomWidgetSection()
@@ -480,6 +490,165 @@ struct VotingLayoutPickerCompact: View {
}
}
// MARK: - Celebration Animation Picker
struct CelebrationAnimationPickerCompact: View {
@AppStorage(UserDefaultsStore.Keys.celebrationAnimation.rawValue, store: GroupUserDefaults.groupDefaults) private var celebrationAnimationIndex: Int = 0
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
@Environment(\.colorScheme) private var colorScheme
// Preview state
@State private var previewAnimation: CelebrationAnimationType?
@State private var previewMood: Mood = .great
@State private var showPreviewCelebration = false
@State private var previewScale: CGFloat = 1.0
@State private var previewOpacity: Double = 1.0
private var currentAnimation: CelebrationAnimationType {
CelebrationAnimationType.fromIndex(celebrationAnimationIndex)
}
var body: some View {
VStack(spacing: 0) {
// Animation style picker
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 10) {
ForEach(CelebrationAnimationType.allCases) { animation in
Button(action: {
selectAnimation(animation)
}) {
VStack(spacing: 6) {
Image(systemName: animation.icon)
.font(.title2)
.frame(width: 44, height: 44)
.foregroundColor(currentAnimation == animation ? animation.accentColor : theme.currentTheme.labelColor.opacity(0.4))
Text(animation.rawValue)
.font(.caption2.weight(.medium))
.foregroundColor(currentAnimation == animation ? animation.accentColor : theme.currentTheme.labelColor.opacity(0.5))
}
.frame(width: 70)
.padding(.vertical, 12)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(currentAnimation == animation
? animation.accentColor.opacity(0.1)
: (colorScheme == .dark ? Color(.systemGray5) : .white))
)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, 4)
}
// Inline animation preview
if let animation = previewAnimation {
AnimationPreviewView(
animation: animation,
mood: previewMood,
moodTint: moodTint,
showCelebration: showPreviewCelebration,
onCelebrationComplete: {
dismissPreview()
}
)
.scaleEffect(previewScale)
.opacity(previewOpacity)
.padding(.top, 16)
.transition(.scale(scale: 0.8).combined(with: .opacity))
}
}
}
private func selectAnimation(_ animation: CelebrationAnimationType) {
// Save preference
if UIAccessibility.isReduceMotionEnabled {
celebrationAnimationIndex = animation.index
} else {
withAnimation(.easeInOut(duration: 0.2)) {
celebrationAnimationIndex = animation.index
}
}
AnalyticsManager.shared.track(.celebrationAnimationChanged(animation: animation.rawValue))
// Reset and show preview
previewScale = 1.0
previewOpacity = 1.0
showPreviewCelebration = false
previewMood = Mood.allValues.randomElement() ?? .great
withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
previewAnimation = animation
}
// Auto-trigger the celebration after a brief pause
Task { @MainActor in
try? await Task.sleep(for: .seconds(0.5))
guard previewAnimation == animation else { return }
withAnimation(.easeInOut(duration: 0.3)) {
showPreviewCelebration = true
}
}
}
private func dismissPreview() {
withAnimation(.easeIn(duration: 0.3)) {
previewScale = 0.6
previewOpacity = 0
}
Task { @MainActor in
try? await Task.sleep(for: .seconds(0.35))
withAnimation(.easeOut(duration: 0.15)) {
previewAnimation = nil
}
}
}
}
// MARK: - Animation Preview (inline in Customize)
private struct AnimationPreviewView: View {
let animation: CelebrationAnimationType
let mood: Mood
let moodTint: MoodTints
let showCelebration: Bool
let onCelebrationComplete: () -> Void
var body: some View {
ZStack {
// Mini voting row (fades out when celebration starts)
HStack(spacing: 12) {
ForEach(Mood.allValues, id: \.self) { m in
m.icon
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 36, height: 36)
.foregroundColor(moodTint.color(forMood: m))
.scaleEffect(showCelebration && m == mood ? 1.3 : 1.0)
.opacity(showCelebration && m != mood ? 0.3 : 1.0)
}
}
.opacity(showCelebration ? 0 : 1)
// Celebration overlay
if showCelebration {
CelebrationOverlayView(
animationType: animation,
mood: mood,
onComplete: onCelebrationComplete
)
}
}
.frame(height: 160)
.frame(maxWidth: .infinity)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(.systemGray6).opacity(0.5))
)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
}
// MARK: - Custom Widget Section
struct CustomWidgetSection: View {
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system