Skip to content

Commit 4f15773

Browse files
authored
Merge pull request #10 from melvinmt/voice
VoiceImplement robust audio-speech coordination system
2 parents b365272 + 8d0a712 commit 4f15773

8 files changed

Lines changed: 1033 additions & 899 deletions

File tree

app.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"supportsTablet": true,
1919
"bundleIdentifier": "com.earlyreader.app",
2020
"infoPlist": {
21-
"ITSAppUsesNonExemptEncryption": false
21+
"ITSAppUsesNonExemptEncryption": false,
22+
"NSPhotoLibraryUsageDescription": "Early Reader uses photos to display learning card images."
2223
}
2324
},
2425
"android": {

ios/EarlyReader/Info.plist

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
</array>
3535
<key>CFBundleVersion</key>
3636
<string>$(CURRENT_PROJECT_VERSION)</string>
37+
<key>ITSAppUsesNonExemptEncryption</key>
38+
<false/>
3739
<key>LSMinimumSystemVersion</key>
3840
<string>15.1</string>
3941
<key>LSRequiresIPhoneOS</key>
@@ -47,6 +49,8 @@
4749
</dict>
4850
<key>NSMicrophoneUsageDescription</key>
4951
<string>Early Reader needs access to your microphone to help your child practice pronunciation.</string>
52+
<key>NSPhotoLibraryUsageDescription</key>
53+
<string>Early Reader uses photos to display learning card images.</string>
5054
<key>NSSpeechRecognitionUsageDescription</key>
5155
<string>Early Reader needs speech recognition to help your child learn to read by practicing pronunciation.</string>
5256
<key>NSUserActivityTypes</key>

src/components/ui/ConfettiCelebration.tsx

Lines changed: 119 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { useEffect, useRef } from 'react';
2-
import { View, StyleSheet, Animated, Dimensions } from 'react-native';
1+
import { useEffect, useState, useRef } from 'react';
2+
import { StyleSheet, Animated, Dimensions } from 'react-native';
33

44
interface ConfettiCelebrationProps {
55
visible: boolean;
@@ -10,136 +10,137 @@ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
1010
const CONFETTI_COUNT = 50;
1111
const COLORS = ['#FFD700', '#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8', '#F7DC6F'];
1212

13-
export default function ConfettiCelebration({ visible, onComplete }: ConfettiCelebrationProps) {
14-
const confettiRefs = useRef<Animated.Value[]>([]);
15-
const opacityRef = useRef(new Animated.Value(0));
13+
interface ConfettiPiece {
14+
id: number;
15+
translateY: Animated.Value;
16+
translateX: Animated.Value;
17+
rotation: Animated.Value;
18+
scale: Animated.Value;
19+
color: string;
20+
size: number;
21+
initialX: number; // Track initial X position (avoid accessing Animated._value)
22+
}
23+
24+
function createConfettiPieces(): ConfettiPiece[] {
25+
return Array.from({ length: CONFETTI_COUNT }, (_, index) => {
26+
const initialX = Math.random() * SCREEN_WIDTH;
27+
return {
28+
id: index,
29+
translateY: new Animated.Value(-50 - Math.random() * 100),
30+
translateX: new Animated.Value(initialX),
31+
rotation: new Animated.Value(0),
32+
scale: new Animated.Value(0.8 + Math.random() * 0.4),
33+
color: COLORS[index % COLORS.length],
34+
size: 8 + Math.random() * 8,
35+
initialX,
36+
};
37+
});
38+
}
39+
40+
export default function ConfettiCelebration({ visible }: ConfettiCelebrationProps) {
41+
const [confetti, setConfetti] = useState<ConfettiPiece[]>([]);
42+
const isAnimatingRef = useRef(false);
43+
const animationRef = useRef<Animated.CompositeAnimation | null>(null);
1644

1745
useEffect(() => {
1846
if (visible) {
19-
// Initialize confetti pieces
20-
confettiRefs.current = Array.from({ length: CONFETTI_COUNT }, () => {
21-
const startX = Math.random() * SCREEN_WIDTH;
22-
return {
23-
translateY: new Animated.Value(-50),
24-
translateX: new Animated.Value(startX),
25-
rotation: new Animated.Value(0),
26-
scale: new Animated.Value(1),
27-
startX, // Store for reference
28-
};
29-
});
47+
// Create confetti immediately so it renders
48+
const pieces = createConfettiPieces();
49+
setConfetti(pieces);
50+
isAnimatingRef.current = true;
51+
52+
// Start animation after a brief delay to ensure render
53+
const startAnimation = () => {
54+
if (!isAnimatingRef.current) return;
55+
56+
const animations = pieces.map((piece) => {
57+
// Reset positions for loop - update initialX for each cycle
58+
const newInitialX = Math.random() * SCREEN_WIDTH;
59+
piece.initialX = newInitialX;
60+
piece.translateY.setValue(-50 - Math.random() * 100);
61+
piece.translateX.setValue(newInitialX);
62+
piece.rotation.setValue(0);
63+
64+
const duration = 2500 + Math.random() * 1500;
65+
const endX = piece.initialX + (Math.random() - 0.5) * 200;
3066

31-
// Start animation
32-
opacityRef.current.setValue(1);
33-
34-
// Animate each confetti piece
35-
const animations = confettiRefs.current.map((confetti, index) => {
36-
const duration = 2000 + Math.random() * 1000;
37-
const delay = index * 20;
38-
const endX = confetti.startX + (Math.random() - 0.5) * 200;
39-
40-
return Animated.parallel([
41-
Animated.timing(confetti.translateY, {
42-
toValue: SCREEN_HEIGHT + 100,
43-
duration,
44-
delay,
45-
useNativeDriver: true,
46-
}),
47-
Animated.timing(confetti.translateX, {
48-
toValue: endX,
49-
duration,
50-
delay,
51-
useNativeDriver: true,
52-
}),
53-
Animated.timing(confetti.rotation, {
54-
toValue: Math.random() * 720 - 360,
55-
duration,
56-
delay,
57-
useNativeDriver: true,
58-
}),
59-
Animated.sequence([
60-
Animated.timing(confetti.scale, {
61-
toValue: 1.2,
62-
duration: duration * 0.3,
63-
delay,
67+
return Animated.parallel([
68+
Animated.timing(piece.translateY, {
69+
toValue: SCREEN_HEIGHT + 100,
70+
duration,
6471
useNativeDriver: true,
6572
}),
66-
Animated.timing(confetti.scale, {
67-
toValue: 0.8,
68-
duration: duration * 0.7,
73+
Animated.timing(piece.translateX, {
74+
toValue: endX,
75+
duration,
6976
useNativeDriver: true,
7077
}),
71-
]),
72-
]);
73-
});
78+
Animated.timing(piece.rotation, {
79+
toValue: Math.random() * 720 - 360,
80+
duration,
81+
useNativeDriver: true,
82+
}),
83+
]);
84+
});
85+
86+
animationRef.current = Animated.parallel(animations);
87+
animationRef.current.start(() => {
88+
// Loop if still visible
89+
if (isAnimatingRef.current) {
90+
startAnimation();
91+
}
92+
});
93+
};
7494

75-
// Fade out after animation
76-
Animated.parallel([
77-
...animations,
78-
Animated.sequence([
79-
Animated.delay(1500),
80-
Animated.timing(opacityRef.current, {
81-
toValue: 0,
82-
duration: 500,
83-
useNativeDriver: true,
84-
}),
85-
]),
86-
]).start(() => {
87-
onComplete?.();
95+
// Small delay to ensure state update has rendered
96+
requestAnimationFrame(() => {
97+
startAnimation();
8898
});
8999
} else {
90-
// Reset
91-
opacityRef.current.setValue(0);
92-
confettiRefs.current.forEach((confetti) => {
93-
confetti.translateY.setValue(-50);
94-
confetti.translateX.setValue(confetti.startX);
95-
confetti.rotation.setValue(0);
96-
confetti.scale.setValue(1);
97-
});
100+
isAnimatingRef.current = false;
101+
if (animationRef.current) {
102+
animationRef.current.stop();
103+
animationRef.current = null;
104+
}
105+
setConfetti([]);
98106
}
99-
}, [visible, onComplete]);
100107

101-
if (!visible) return null;
108+
return () => {
109+
isAnimatingRef.current = false;
110+
if (animationRef.current) {
111+
animationRef.current.stop();
112+
}
113+
};
114+
}, [visible]);
115+
116+
if (!visible || confetti.length === 0) return null;
102117

103118
return (
104-
<Animated.View
105-
style={[styles.container, { opacity: opacityRef.current }]}
106-
pointerEvents="none"
107-
>
108-
{confettiRefs.current.map((confetti, index) => {
109-
const color = COLORS[index % COLORS.length];
110-
const size = 8 + Math.random() * 8;
111-
112-
return (
113-
<Animated.View
114-
key={index}
115-
style={[
116-
styles.confetti,
117-
{
118-
width: size,
119-
height: size,
120-
backgroundColor: color,
121-
transform: [
122-
{
123-
translateY: confetti.translateY,
124-
},
125-
{
126-
translateX: confetti.translateX,
127-
},
128-
{
129-
rotate: confetti.rotation.interpolate({
130-
inputRange: [-360, 360],
131-
outputRange: ['-360deg', '360deg'],
132-
}),
133-
},
134-
{
135-
scale: confetti.scale,
136-
},
137-
],
138-
},
139-
]}
140-
/>
141-
);
142-
})}
119+
<Animated.View style={styles.container} pointerEvents="none">
120+
{confetti.map((piece) => (
121+
<Animated.View
122+
key={piece.id}
123+
style={[
124+
styles.confetti,
125+
{
126+
width: piece.size,
127+
height: piece.size,
128+
backgroundColor: piece.color,
129+
transform: [
130+
{ translateY: piece.translateY },
131+
{ translateX: piece.translateX },
132+
{
133+
rotate: piece.rotation.interpolate({
134+
inputRange: [-360, 360],
135+
outputRange: ['-360deg', '360deg'],
136+
}),
137+
},
138+
{ scale: piece.scale },
139+
],
140+
},
141+
]}
142+
/>
143+
))}
143144
</Animated.View>
144145
);
145146
}
@@ -156,7 +157,7 @@ const styles = StyleSheet.create({
156157
},
157158
confetti: {
158159
position: 'absolute',
160+
top: 0,
159161
borderRadius: 2,
160162
},
161163
});
162-

0 commit comments

Comments
 (0)