diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c92b99d6..88caa502 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -952,5 +952,45 @@ "example": "3" } } + }, + "sampleMessage": "This is a sample message for text to speech.", + "@sampleMessage": { + "description": "Sample message for text to speech" + }, + "actionMessage": "{author} is performing an action: {text}", + "@actionMessage": { + "description": "Message for an action performed by the author", + "placeholders": { + "author": { + "type": "String", + "example": "JohnDoe" + }, + "text": { + "type": "String", + "example": "is dancing" + } + } + }, + "saidMessage": "{author} said: {text}", + "@saidMessage": { + "description": "Message for something said by the author", + "placeholders": { + "author": { + "type": "String", + "example": "JohnDoe" + }, + "text": { + "type": "String", + "example": "Hello everyone!" + } + } + }, + "textToSpeechEnabled": "Text to speech enabled", + "@textToSpeechEnabled": { + "description": "Message indicating that text to speech has been enabled" + }, + "textToSpeechDisabled": "Text to speech disabled", + "@textToSpeechDisabled": { + "description": "Message indicating that text to speech has been disabled" } } diff --git a/lib/main.dart b/lib/main.dart index 63a9c3e3..c79d7ecb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -68,13 +68,18 @@ void updateChannelSubscription(String? data) { StreamController channelStreamController = StreamController.broadcast(); +final GlobalKey navigatorKey = GlobalKey(); + void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await MobileAds.instance.initialize(); final prefs = await StreamingSharedPreferences.instance; + + final currentLocale = PlatformDispatcher.instance.locale; + await tts_isolate.isolateMain( - ReceivePort().sendPort, channelStreamController, prefs); + ReceivePort().sendPort, channelStreamController, prefs, currentLocale); if (!kDebugMode) { FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError; @@ -293,6 +298,7 @@ class _AppState extends State { ], child: Consumer(builder: (context, layoutModel, child) { return MaterialApp( + navigatorKey: navigatorKey, title: 'RealtimeChat', theme: Themes.lightTheme, darkTheme: Themes.darkTheme, diff --git a/lib/models/messages.dart b/lib/models/messages.dart index bc0339ec..04540741 100644 --- a/lib/models/messages.dart +++ b/lib/models/messages.dart @@ -2,7 +2,9 @@ import 'dart:async'; import 'dart:core'; import 'dart:math'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:rtchat/main.dart'; import 'package:rtchat/models/adapters/messages.dart'; import 'package:rtchat/models/adapters/profiles.dart'; import 'package:rtchat/models/channels.dart'; @@ -34,7 +36,14 @@ class MessagesModel extends ChangeNotifier { _separators = {}; _events = []; _isLive = false; - _tts?.enabled = false; + if (_tts != null) { + final localizations = _getLocalizations(); + if (localizations != null) { + _tts!.setEnabled(localizations, false); + } else { + debugPrint("Localizations not available"); + } + } notifyListeners(); _subscription?.cancel(); @@ -52,7 +61,13 @@ class MessagesModel extends ChangeNotifier { _messages.insert(index, event.model); } else { _messages.add(event.model); - _tts?.say(event.model); + // Pass localizations to the TTS say method + final localizations = _getLocalizations(); + if (localizations != null) { + _tts?.say(localizations, event.model); + } else { + debugPrint("Localizations not available"); + } if (_isLive && shouldPing()) { ProfilesAdapter.instance .getIsOnline(channelId: channel.toString()) @@ -199,7 +214,14 @@ class MessagesModel extends ChangeNotifier { return; } _tts = tts; - tts?.enabled = false; + if (_tts != null) { + final localizations = _getLocalizations(); + if (localizations != null) { + _tts!.setEnabled(localizations, false); + } else { + debugPrint("Localizations not available"); + } + } notifyListeners(); } @@ -252,4 +274,16 @@ class MessagesModel extends ChangeNotifier { "announcementPinDuration": _announcementPinDuration.inSeconds.toInt(), "pingMinGapDuration": _pingMinGapDuration.inSeconds.toInt(), }; + + BuildContext? _getContext() { + return navigatorKey.currentContext; + } + + AppLocalizations? _getLocalizations() { + final context = _getContext(); + if (context != null) { + return AppLocalizations.of(context); + } + return null; + } } diff --git a/lib/models/tts.dart b/lib/models/tts.dart index a01a76ae..d9824813 100644 --- a/lib/models/tts.dart +++ b/lib/models/tts.dart @@ -17,6 +17,7 @@ import 'package:rtchat/models/tts/language.dart'; import 'package:rtchat/models/tts/bytes_audio_source.dart'; import 'package:rtchat/models/user.dart'; import 'package:flutter_tts/flutter_tts.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class TtsModel extends ChangeNotifier { var _isCloudTtsEnabled = false; @@ -104,7 +105,7 @@ class TtsModel extends ChangeNotifier { notifyListeners(); } - String getVocalization(MessageModel model, + String getVocalization(AppLocalizations l10n, MessageModel model, {bool includeAuthorPrelude = false}) { if (model is TwitchMessageModel) { final text = model.tokenized @@ -131,9 +132,14 @@ class TtsModel extends ChangeNotifier { if (!includeAuthorPrelude || isPreludeMuted) { return text; } - return model.isAction ? "$author $text" : "$author said $text"; + return model.isAction + ? l10n.actionMessage(author, text) + : l10n.saidMessage(author, text); } else if (model is StreamStateEventModel) { - return model.isOnline ? "Stream is online" : "Stream is offline"; + final timestamp = model.timestamp; + return model.isOnline + ? l10n.streamOnline(timestamp, timestamp) + : l10n.streamOffline(timestamp, timestamp); } else if (model is SystemMessageModel) { return model.text; } @@ -159,7 +165,7 @@ class TtsModel extends ChangeNotifier { return _isEnabled; } - set enabled(bool value) { + void setEnabled(AppLocalizations localizations, bool value) { if (value == _isEnabled) { return; } @@ -168,8 +174,11 @@ class TtsModel extends ChangeNotifier { _lastMessageTime = DateTime.now(); } say( + localizations, SystemMessageModel( - text: "Text to speech ${value ? "enabled" : "disabled"}"), + text: value + ? localizations.textToSpeechEnabled + : localizations.textToSpeechDisabled), force: true); WidgetsBinding.instance.addPostFrameCallback((_) { notifyListeners(); @@ -285,7 +294,8 @@ class TtsModel extends ChangeNotifier { } } - void say(MessageModel model, {bool force = false}) async { + void say(AppLocalizations localizations, MessageModel model, + {bool force = false}) async { if (!enabled && !force) { return; } @@ -316,8 +326,11 @@ class TtsModel extends ChangeNotifier { includeAuthorPrelude = !(activeMessage.author == model.author); } - final vocalization = - getVocalization(model, includeAuthorPrelude: includeAuthorPrelude); + final vocalization = getVocalization( + localizations, + model, + includeAuthorPrelude: includeAuthorPrelude, + ); // if the vocalization is empty, skip the message if (vocalization.isEmpty) { diff --git a/lib/notifications_plugin.dart b/lib/notifications_plugin.dart index 7e10c6a0..354f701b 100644 --- a/lib/notifications_plugin.dart +++ b/lib/notifications_plugin.dart @@ -16,7 +16,7 @@ class NotificationsPlugin { } } - static Future listenToTTs(TtsModel model) async { + static Future listenToTts(TtsModel model) async { try { debugPrint("Listening to TTS"); diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 14d26f41..e693b284 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -175,7 +175,7 @@ class _HomeScreenState extends State with TickerProviderStateMixin { final model = Provider.of(context, listen: false); final ttsModel = Provider.of(context, listen: false); - NotificationsPlugin.listenToTTs(ttsModel); + NotificationsPlugin.listenToTts(ttsModel); if (model.sources.isEmpty || (await AudioChannel.hasPermission())) { return; @@ -265,7 +265,8 @@ class _HomeScreenState extends State with TickerProviderStateMixin { tooltip: AppLocalizations.of(context)!.textToSpeech, onPressed: () async { if (!kDebugMode) { - ttsModel.enabled = !ttsModel.enabled; + ttsModel.setEnabled(AppLocalizations.of(context)!, + ttsModel.enabled ? false : true); // Toggle newTtsEnabled and notify listeners immediately } else { ttsModel.newTtsEnabled = !ttsModel.newTtsEnabled; @@ -290,7 +291,7 @@ class _HomeScreenState extends State with TickerProviderStateMixin { "${userModel.activeChannel?.provider}:${userModel.activeChannel?.channelId}", ); NotificationsPlugin.showNotification(); - NotificationsPlugin.listenToTTs(ttsModel); + NotificationsPlugin.listenToTts(ttsModel); } } }, diff --git a/lib/screens/settings/tts.dart b/lib/screens/settings/tts.dart index e0261c20..a42a1193 100644 --- a/lib/screens/settings/tts.dart +++ b/lib/screens/settings/tts.dart @@ -8,6 +8,7 @@ import 'package:provider/provider.dart'; import 'package:rtchat/models/messages/message.dart'; import 'package:rtchat/models/tts.dart'; import 'package:rtchat/models/tts/bytes_audio_source.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class TextToSpeechScreen extends StatelessWidget { const TextToSpeechScreen({super.key}); @@ -154,16 +155,16 @@ class TextToSpeechScreen extends StatelessWidget { "voice": model.voice, "rate": model.speed * 1.5 + 0.5, "pitch": model.pitch * 4 - 2, - "text": - "kevin calmly and collectively consumes cheesecake", + "text": AppLocalizations.of(context)!.sampleMessage, }); final bytes = const Base64Decoder().convert(response.data); audioPlayer.setAudioSource(BytesAudioSource(bytes)); audioPlayer.play(); } else { model.say( + AppLocalizations.of(context)!, SystemMessageModel( - text: "muxfd said have you followed muxfd on twitch?", + text: AppLocalizations.of(context)!.sampleMessage, ), force: true); } diff --git a/lib/tts_isolate.dart b/lib/tts_isolate.dart index 97c309fd..06c3b7ca 100644 --- a/lib/tts_isolate.dart +++ b/lib/tts_isolate.dart @@ -6,32 +6,35 @@ import 'dart:ui'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:rtchat/models/tts.dart'; import 'package:rtchat/models/messages/twitch/message.dart'; import 'package:rtchat/models/messages/twitch/user.dart'; import 'package:rtchat/models/messages/twitch/reply.dart'; import 'package:rtchat/tts_plugin.dart'; - import 'package:streaming_shared_preferences/streaming_shared_preferences.dart'; final DateTime ttsTimeStampListener = DateTime.now(); StreamSubscription? messagesSubscription; StreamSubscription? channelSubscription; -@pragma("vm:entry-point") Future isolateMain( SendPort sendPort, StreamController channelStream, - StreamingSharedPreferences prefs) async { + StreamingSharedPreferences prefs, + Locale currentLocale) async { DartPluginRegistrant.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); + + final localizations = await AppLocalizations.delegate.load(currentLocale); + final ttsQueue = TTSQueue(); final ttsModel = TtsModel.fromJson( jsonDecode(prefs.getString("tts", defaultValue: '{}').getValue())); - // listen for changes to the tts preferences and update the isolates ttsModel + // Listen for changes to the tts preferences and update the isolates ttsModel final ttsPrefs = prefs.getString('tts', defaultValue: '{}'); ttsPrefs.listen((value) async { ttsModel.updateFromJson(jsonDecode(value)); @@ -93,6 +96,7 @@ Future isolateMain( return; // Skip vocalization for bot messages } final finalMessage = ttsModel.getVocalization( + localizations, messageModel, includeAuthorPrelude: !ttsModel.isPreludeMuted, ); diff --git a/pubspec.yaml b/pubspec.yaml index 9cfe1675..e32e793e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,8 +59,8 @@ dev_dependencies: sdk: flutter flutter: + uses-material-design: true + generate: true assets: - assets/ - assets/providers/ - generate: true - uses-material-design: true