first release 0.0.0.2

This commit is contained in:
2026-03-08 23:31:46 +01:00
parent ea52a4cec4
commit 5eb3ebf199
1432 changed files with 99065 additions and 60 deletions
+176
View File
@@ -0,0 +1,176 @@
/**
* VoiceButton - Push-to-Talk Aufnahmeknopf
*
* Grosser runder Button: gedrueckt halten zum Aufnehmen, loslassen zum Senden.
* Visuelles Feedback durch pulsierende Animation waehrend der Aufnahme.
*/
import React, { useState, useRef, useEffect } from 'react';
import {
View,
Text,
Animated,
StyleSheet,
GestureResponderEvent,
Easing,
} from 'react-native';
import audioService, { RecordingResult } from '../services/audio';
// --- Typen ---
interface VoiceButtonProps {
/** Wird aufgerufen wenn die Aufnahme fertig ist */
onRecordingComplete: (result: RecordingResult) => void;
/** Button deaktivieren */
disabled?: boolean;
}
// --- Komponente ---
const VoiceButton: React.FC<VoiceButtonProps> = ({ onRecordingComplete, disabled = false }) => {
const [isRecording, setIsRecording] = useState(false);
const [durationMs, setDurationMs] = useState(0);
const pulseAnim = useRef(new Animated.Value(1)).current;
const durationTimer = useRef<ReturnType<typeof setInterval> | null>(null);
// Puls-Animation starten/stoppen
useEffect(() => {
if (isRecording) {
const pulse = Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, {
toValue: 1.2,
duration: 600,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
Animated.timing(pulseAnim, {
toValue: 1,
duration: 600,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
]),
);
pulse.start();
return () => pulse.stop();
} else {
pulseAnim.setValue(1);
}
}, [isRecording, pulseAnim]);
// Aufnahmedauer zaehlen
useEffect(() => {
if (isRecording) {
setDurationMs(0);
durationTimer.current = setInterval(() => {
setDurationMs(prev => prev + 100);
}, 100);
} else {
if (durationTimer.current) {
clearInterval(durationTimer.current);
durationTimer.current = null;
}
}
return () => {
if (durationTimer.current) {
clearInterval(durationTimer.current);
}
};
}, [isRecording]);
const handlePressIn = async (_event: GestureResponderEvent) => {
if (disabled) return;
const started = await audioService.startRecording();
if (started) {
setIsRecording(true);
}
};
const handlePressOut = async (_event: GestureResponderEvent) => {
if (!isRecording) return;
setIsRecording(false);
const result = await audioService.stopRecording();
if (result && result.durationMs > 300) {
// Nur senden wenn laenger als 300ms (versehentliches Tippen vermeiden)
onRecordingComplete(result);
}
};
const formatDuration = (ms: number): string => {
const seconds = Math.floor(ms / 1000);
const tenths = Math.floor((ms % 1000) / 100);
return `${seconds}.${tenths}s`;
};
return (
<View style={styles.container}>
<Animated.View
style={[
styles.buttonOuter,
isRecording && styles.buttonOuterRecording,
{ transform: [{ scale: pulseAnim }] },
]}
onStartShouldSetResponder={() => true}
onResponderGrant={handlePressIn}
onResponderRelease={handlePressOut}
onResponderTerminate={handlePressOut}
>
<View style={[styles.buttonInner, isRecording && styles.buttonInnerRecording]}>
<Text style={styles.buttonIcon}>{isRecording ? '⏹' : '🎙'}</Text>
</View>
</Animated.View>
{isRecording && (
<Text style={styles.durationText}>{formatDuration(durationMs)}</Text>
)}
</View>
);
};
// --- Styles ---
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
},
buttonOuter: {
width: 64,
height: 64,
borderRadius: 32,
backgroundColor: 'rgba(0, 150, 255, 0.2)',
alignItems: 'center',
justifyContent: 'center',
},
buttonOuterRecording: {
backgroundColor: 'rgba(255, 59, 48, 0.3)',
},
buttonInner: {
width: 52,
height: 52,
borderRadius: 26,
backgroundColor: '#0096FF',
alignItems: 'center',
justifyContent: 'center',
elevation: 4,
shadowColor: '#0096FF',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.4,
shadowRadius: 4,
},
buttonInnerRecording: {
backgroundColor: '#FF3B30',
},
buttonIcon: {
fontSize: 24,
},
durationText: {
color: '#FF3B30',
fontSize: 12,
marginTop: 4,
fontVariant: ['tabular-nums'],
},
});
export default VoiceButton;