PraxaLing / mobile /lib /widgets /tts_button.dart
Reubencf's picture
Deploy PraxaLing: component refactor, bug fixes, unified neo-brutal design, Qwen3.5 everywhere
2992997
Raw
History Blame Contribute Delete
2.86 kB
import 'package:flutter/material.dart';
import 'package:flutter_tts/flutter_tts.dart';
import '../theme/app_theme.dart';
const _ttsLangMap = <String, String>{
'en': 'en-US',
'es': 'es-ES',
'fr': 'fr-FR',
'de': 'de-DE',
'it': 'it-IT',
'pt': 'pt-BR',
'ja': 'ja-JP',
'ko': 'ko-KR',
'zh': 'zh-CN',
'yue': 'zh-HK',
'ar': 'ar-SA',
'hi': 'hi-IN',
'kn': 'kn-IN',
'ml': 'ml-IN',
'vi': 'vi-VN',
'ru': 'ru-RU',
};
String ttsLang(String code) => _ttsLangMap[code] ?? code;
class TtsButton extends StatefulWidget {
const TtsButton({super.key, required this.text, required this.lang, this.size = 36});
final String text;
final String lang;
final double size;
@override
State<TtsButton> createState() => _TtsButtonState();
}
class _TtsButtonState extends State<TtsButton> {
late final FlutterTts _tts;
bool _speaking = false;
bool _pressed = false;
@override
void initState() {
super.initState();
_tts = FlutterTts();
_tts.setCompletionHandler(() {
if (mounted) setState(() => _speaking = false);
});
_tts.setCancelHandler(() {
if (mounted) setState(() => _speaking = false);
});
_tts.setErrorHandler((_) {
if (mounted) setState(() => _speaking = false);
});
}
@override
void dispose() {
_tts.stop();
super.dispose();
}
Future<void> _toggle() async {
if (_speaking) {
await _tts.stop();
if (mounted) setState(() => _speaking = false);
return;
}
await _tts.stop();
await _tts.setLanguage(ttsLang(widget.lang));
await _tts.setSpeechRate(0.45);
setState(() => _speaking = true);
final result = await _tts.speak(widget.text);
if (result != 1 && mounted) setState(() => _speaking = false);
}
@override
Widget build(BuildContext context) {
final offset = _pressed ? 1.0 : 2.0;
final fill = _speaking ? const Color(0xFFFF8080) : kBrutalYellow;
return GestureDetector(
onTapDown: (_) => setState(() => _pressed = true),
onTapUp: (_) => setState(() => _pressed = false),
onTapCancel: () => setState(() => _pressed = false),
onTap: _toggle,
child: AnimatedContainer(
duration: const Duration(milliseconds: 80),
width: widget.size,
height: widget.size,
transform: Matrix4.translationValues(_pressed ? 1 : 0, _pressed ? 1 : 0, 0),
decoration: BoxDecoration(
color: fill,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: kBrutalBlack, width: 2),
boxShadow: [
BoxShadow(color: kBrutalBlack, offset: Offset(offset, offset), blurRadius: 0),
],
),
child: Icon(
_speaking ? Icons.stop_rounded : Icons.volume_up_rounded,
color: kBrutalBlack,
size: widget.size * 0.55,
),
),
);
}
}