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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user