import 'dart:async'; import 'dart:js_interop'; import 'package:flutter/foundation.dart'; import 'spelling_tts_backend.dart'; SpellingTtsBackend createSpellingTtsBackend() => _WebSpeechBackend(); @JS('speechSynthesis') external SpeechSynthesis get _speechSynthesis; @JS() extension type SpeechSynthesis._(JSObject _) implements JSObject { external void speak(SpeechSynthesisUtterance utterance); external void cancel(); } @JS() extension type SpeechSynthesisUtterance._(JSObject _) implements JSObject { external SpeechSynthesisUtterance(); external String text; external String lang; external double rate; external double pitch; @JS('onend') external set onEnd(JSFunction? listener); @JS('onerror') external set onError(JSFunction? listener); } class _WebSpeechBackend implements SpellingTtsBackend { Completer? _pending; @override Future speak(String text, {String? title}) async { final String trimmed = text.trim(); if (trimmed.isEmpty) { return; } if (title != null && title.isNotEmpty) { debugPrint('Spelling TTS utterance title: $title'); } await stop(); final Completer done = Completer(); _pending = done; final SpeechSynthesisUtterance utterance = SpeechSynthesisUtterance(); utterance.text = trimmed; utterance.lang = 'en-US'; utterance.rate = 0.85; utterance.pitch = 1.0; utterance.onEnd = (() { if (!done.isCompleted) { done.complete(); } }).toJS; utterance.onError = ((JSAny event) { debugPrint('Spelling web TTS error: $event'); if (!done.isCompleted) { done.complete(); } }).toJS; try { _speechSynthesis.speak(utterance); await done.future.timeout( const Duration(seconds: 30), onTimeout: () { debugPrint('Spelling web TTS timed out'); }, ); } catch (e, st) { debugPrint('Spelling web TTS failed: $e\n$st'); if (!done.isCompleted) { done.complete(); } } finally { if (identical(_pending, done)) { _pending = null; } } } @override Future stop() async { final Completer? pending = _pending; _pending = null; try { _speechSynthesis.cancel(); } catch (e, st) { debugPrint('Spelling web TTS stop failed: $e\n$st'); } pending?.complete(); } }