added enabled tts learning setting, give user a warning message when tts not available for target language (#1227)

pull/1544/head
ggurdin 11 months ago committed by GitHub
parent 43040c4010
commit 9444aecfd3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -4058,7 +4058,7 @@
"roomDataMissing": "Some data may be missing from rooms in which you are not a member.", "roomDataMissing": "Some data may be missing from rooms in which you are not a member.",
"updatePhoneOS": "You may need to update your device's OS version.", "updatePhoneOS": "You may need to update your device's OS version.",
"wordsPerMinute": "Words per minute", "wordsPerMinute": "Words per minute",
"autoIGCToolName": "Run Language Assistance Automatically", "autoIGCToolName": "Run language assistance automatically",
"autoIGCToolDescription": "Automatically run language assistance after typing messages", "autoIGCToolDescription": "Automatically run language assistance after typing messages",
"runGrammarCorrection": "Check message", "runGrammarCorrection": "Check message",
"grammarCorrectionFailed": "Issues to address", "grammarCorrectionFailed": "Issues to address",
@ -4623,5 +4623,9 @@
"maxXP": {} "maxXP": {}
} }
}, },
"registrationEmailMessage": "Please verify your email with a link sent there. In some cases, the email takes up to 5 minutes to arrive. Please also check your spam folder." "registrationEmailMessage": "Please verify your email with a link sent there. In some cases, the email takes up to 5 minutes to arrive. Please also check your spam folder.",
"enableTTSToolName": "Enabled text-to-speech",
"enableTTSToolDescription": "Allow the app to generate text-to-speech output for portions of text in your target language",
"couldNotFindTTS": "We couldn't find a text-to-speech engine for your current target language. ",
"ttsInstructionsHyperlink": "Click here to view instructions for downloading a new voice on your device."
} }

@ -159,6 +159,11 @@ abstract class AppConfig {
static String androidUpdateURL = static String androidUpdateURL =
"https://play.google.com/store/apps/details?id=com.talktolearn.chat"; "https://play.google.com/store/apps/details?id=com.talktolearn.chat";
static String iosUpdateURL = "itms-apps://itunes.apple.com/app/id1445118630"; static String iosUpdateURL = "itms-apps://itunes.apple.com/app/id1445118630";
static String windowsTTSDownloadInstructions =
"https://support.microsoft.com/en-us/topic/download-languages-and-voices-for-immersive-reader-read-mode-and-read-aloud-4c83a8d8-7486-42f7-8e46-2b0fdf753130";
static String androidTTSDownloadInstructions =
"https://support.google.com/accessibility/android/answer/6006983?hl=en";
// Pangea# // Pangea#
static void loadFromJson(Map<String, dynamic> json) { static void loadFromJson(Map<String, dynamic> json) {

@ -232,6 +232,7 @@ enum ToolSetting {
definitions, definitions,
// translations, // translations,
autoIGC, autoIGC,
enableTTS,
} }
extension SettingCopy on ToolSetting { extension SettingCopy on ToolSetting {
@ -249,6 +250,8 @@ extension SettingCopy on ToolSetting {
// return L10n.of(context).messageTranslationsToolName; // return L10n.of(context).messageTranslationsToolName;
case ToolSetting.autoIGC: case ToolSetting.autoIGC:
return L10n.of(context).autoIGCToolName; return L10n.of(context).autoIGCToolName;
case ToolSetting.enableTTS:
return L10n.of(context).enableTTSToolName;
} }
} }
@ -267,6 +270,8 @@ extension SettingCopy on ToolSetting {
// return L10n.of(context).translationsToolDescrption; // return L10n.of(context).translationsToolDescrption;
case ToolSetting.autoIGC: case ToolSetting.autoIGC:
return L10n.of(context).autoIGCToolDescription; return L10n.of(context).autoIGCToolDescription;
case ToolSetting.enableTTS:
return L10n.of(context).enableTTSToolDescription;
} }
} }
@ -278,6 +283,7 @@ extension SettingCopy on ToolSetting {
case ToolSetting.immersionMode: case ToolSetting.immersionMode:
return false; return false;
case ToolSetting.autoIGC: case ToolSetting.autoIGC:
case ToolSetting.enableTTS:
return true; return true;
} }
} }

@ -126,6 +126,7 @@ class UserToolSettings {
bool immersionMode; bool immersionMode;
bool definitions; bool definitions;
bool autoIGC; bool autoIGC;
bool enableTTS;
UserToolSettings({ UserToolSettings({
this.interactiveTranslator = true, this.interactiveTranslator = true,
@ -133,6 +134,7 @@ class UserToolSettings {
this.immersionMode = false, this.immersionMode = false,
this.definitions = true, this.definitions = true,
this.autoIGC = true, this.autoIGC = true,
this.enableTTS = true,
}); });
factory UserToolSettings.fromJson(Map<String, dynamic> json) => factory UserToolSettings.fromJson(Map<String, dynamic> json) =>
@ -144,6 +146,7 @@ class UserToolSettings {
immersionMode: false, immersionMode: false,
definitions: json[ToolSetting.definitions.toString()] ?? true, definitions: json[ToolSetting.definitions.toString()] ?? true,
autoIGC: json[ToolSetting.autoIGC.toString()] ?? true, autoIGC: json[ToolSetting.autoIGC.toString()] ?? true,
enableTTS: json[ToolSetting.enableTTS.toString()] ?? true,
); );
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@ -153,6 +156,7 @@ class UserToolSettings {
data[ToolSetting.immersionMode.toString()] = immersionMode; data[ToolSetting.immersionMode.toString()] = immersionMode;
data[ToolSetting.definitions.toString()] = definitions; data[ToolSetting.definitions.toString()] = definitions;
data[ToolSetting.autoIGC.toString()] = autoIGC; data[ToolSetting.autoIGC.toString()] = autoIGC;
data[ToolSetting.enableTTS.toString()] = enableTTS;
return data; return data;
} }

@ -3,6 +3,7 @@ import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/models/space_model.dart'; import 'package:fluffychat/pangea/models/space_model.dart';
import 'package:fluffychat/pangea/models/user_model.dart'; import 'package:fluffychat/pangea/models/user_model.dart';
import 'package:fluffychat/pangea/pages/settings_learning/settings_learning_view.dart'; import 'package:fluffychat/pangea/pages/settings_learning/settings_learning_view.dart';
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart'; import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -16,6 +17,19 @@ class SettingsLearning extends StatefulWidget {
class SettingsLearningController extends State<SettingsLearning> { class SettingsLearningController extends State<SettingsLearning> {
PangeaController pangeaController = MatrixState.pangeaController; PangeaController pangeaController = MatrixState.pangeaController;
final tts = TtsController();
@override
void initState() {
super.initState();
tts.setupTTS().then((_) => setState(() {}));
}
@override
void dispose() {
tts.dispose();
super.dispose();
}
setPublicProfile(bool isPublic) { setPublicProfile(bool isPublic) {
pangeaController.userController.updateProfile((profile) { pangeaController.userController.updateProfile((profile) {
@ -50,6 +64,8 @@ class SettingsLearningController extends State<SettingsLearning> {
return profile..toolSettings.definitions = value; return profile..toolSettings.definitions = value;
case ToolSetting.autoIGC: case ToolSetting.autoIGC:
return profile..toolSettings.autoIGC = value; return profile..toolSettings.autoIGC = value;
case ToolSetting.enableTTS:
return profile..toolSettings.enableTTS = value;
} }
}); });
} }
@ -67,6 +83,8 @@ class SettingsLearningController extends State<SettingsLearning> {
return toolSettings.definitions; return toolSettings.definitions;
case ToolSetting.autoIGC: case ToolSetting.autoIGC:
return toolSettings.autoIGC; return toolSettings.autoIGC;
case ToolSetting.enableTTS:
return toolSettings.enableTTS;
} }
} }

@ -1,12 +1,18 @@
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/models/space_model.dart'; import 'package:fluffychat/pangea/models/space_model.dart';
import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart'; import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart';
import 'package:fluffychat/pangea/widgets/user_settings/country_picker_tile.dart'; import 'package:fluffychat/pangea/widgets/user_settings/country_picker_tile.dart';
import 'package:fluffychat/pangea/widgets/user_settings/language_tile.dart'; import 'package:fluffychat/pangea/widgets/user_settings/language_tile.dart';
import 'package:fluffychat/pangea/widgets/user_settings/p_settings_switch_list_tile.dart'; import 'package:fluffychat/pangea/widgets/user_settings/p_settings_switch_list_tile.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:url_launcher/url_launcher_string.dart';
class SettingsLearningView extends StatelessWidget { class SettingsLearningView extends StatelessWidget {
final SettingsLearningController controller; final SettingsLearningController controller;
@ -14,89 +20,122 @@ class SettingsLearningView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final dialogContent = Scaffold( return StreamBuilder(
appBar: AppBar( stream: Matrix.of(context).client.onSync.stream.where((update) {
centerTitle: true, return update.accountData != null &&
title: Text( update.accountData!.any(
L10n.of(context).learningSettings, (event) => event.type == ModelKey.userProfile,
), );
leading: IconButton( }),
icon: const Icon(Icons.close), builder: (context, _) {
onPressed: Navigator.of(context).pop, final dialogContent = Scaffold(
), appBar: AppBar(
), centerTitle: true,
body: ListTileTheme( title: Text(
iconColor: Theme.of(context).textTheme.bodyLarge!.color, L10n.of(context).learningSettings,
child: MaxWidthBody( ),
withScrolling: true, leading: IconButton(
child: Column( icon: const Icon(Icons.close),
children: [ onPressed: Navigator.of(context).pop,
LanguageTile(controller), ),
CountryPickerTile(controller), ),
const Divider(height: 1), body: ListTileTheme(
ListTile( iconColor: Theme.of(context).textTheme.bodyLarge!.color,
title: Text(L10n.of(context).toggleToolSettingsDescription), child: MaxWidthBody(
withScrolling: true,
child: Column(
children: [
LanguageTile(controller),
CountryPickerTile(controller),
const Divider(height: 1),
ListTile(
title: Text(L10n.of(context).toggleToolSettingsDescription),
),
for (final toolSetting in ToolSetting.values
.where((tool) => tool.isAvailableSetting))
Column(
children: [
ProfileSettingsSwitchListTile.adaptive(
defaultValue: controller.getToolSetting(toolSetting),
title: toolSetting.toolName(context),
subtitle: toolSetting == ToolSetting.enableTTS &&
!controller.tts.isLanguageFullySupported
? null
: toolSetting.toolDescription(context),
onChange: (bool value) =>
controller.updateToolSetting(
toolSetting,
value,
),
enabled: toolSetting == ToolSetting.enableTTS
? controller.tts.isLanguageFullySupported
: true,
),
if (toolSetting == ToolSetting.enableTTS &&
!controller.tts.isLanguageFullySupported)
ListTile(
trailing: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Icon(Icons.info_outlined),
),
subtitle: RichText(
text: TextSpan(
text: L10n.of(context).couldNotFindTTS,
style: DefaultTextStyle.of(context).style,
children: [
if (PlatformInfos.isWindows ||
PlatformInfos.isAndroid)
TextSpan(
text: L10n.of(context)
.ttsInstructionsHyperlink,
style: const TextStyle(
color: Colors.blue,
fontWeight: FontWeight.bold,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () {
launchUrlString(
PlatformInfos.isWindows
? AppConfig
.windowsTTSDownloadInstructions
: AppConfig
.androidTTSDownloadInstructions,
);
},
),
],
),
),
),
],
),
],
), ),
for (final toolSetting in ToolSetting.values ),
.where((tool) => tool.isAvailableSetting)) ),
ProfileSettingsSwitchListTile.adaptive( );
defaultValue: controller.getToolSetting(toolSetting),
title: toolSetting.toolName(context), return kIsWeb
subtitle: toolSetting.toolDescription(context), ? Dialog(
onChange: (bool value) => controller.updateToolSetting( child: ConstrainedBox(
toolSetting, constraints: const BoxConstraints(
value, maxWidth: 600,
maxHeight: 600,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20.0),
child: dialogContent,
), ),
), ),
// ProfileSettingsSwitchListTile.adaptive( )
// defaultValue: controller.pangeaController.userController.profile : Dialog.fullscreen(
// .userSettings.itAutoPlay, child: ConstrainedBox(
// title: constraints: const BoxConstraints(maxWidth: 600),
// L10n.of(context).interactiveTranslatorAutoPlaySliderHeader, child: dialogContent,
// subtitle: L10n.of(context).interactiveTranslatorAutoPlayDesc, ),
// onChange: (bool value) => controller );
// .pangeaController.userController },
// .updateProfile((profile) {
// profile.userSettings.itAutoPlay = value;
// return profile;
// }),
// ),
// ProfileSettingsSwitchListTile.adaptive(
// defaultValue: controller.pangeaController.userController.profile
// .userSettings.autoPlayMessages,
// title: L10n.of(context).autoPlayTitle,
// subtitle: L10n.of(context).autoPlayDesc,
// onChange: (bool value) => controller
// .pangeaController.userController
// .updateProfile((profile) {
// profile.userSettings.autoPlayMessages = value;
// return profile;
// }),
// ),
],
),
),
),
); );
return kIsWeb
? Dialog(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 600,
maxHeight: 600,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20.0),
child: dialogContent,
),
),
)
: Dialog.fullscreen(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: dialogContent,
),
);
} }
} }

@ -1,5 +1,7 @@
import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'package:fluffychat/pangea/controllers/user_controller.dart';
import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/enum/instructions_enum.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/widgets/chat/missing_voice_button.dart'; import 'package:fluffychat/pangea/widgets/chat/missing_voice_button.dart';
@ -18,17 +20,24 @@ class TtsController {
List<String> _availableLangCodes = []; List<String> _availableLangCodes = [];
final flutter_tts.FlutterTts _tts = flutter_tts.FlutterTts(); final flutter_tts.FlutterTts _tts = flutter_tts.FlutterTts();
final TextToSpeech _alternativeTTS = TextToSpeech(); final TextToSpeech _alternativeTTS = TextToSpeech();
StreamSubscription? _languageSubscription;
UserController get userController =>
MatrixState.pangeaController.userController;
TtsController() { TtsController() {
setupTTS(); setupTTS();
_languageSubscription =
userController.stateStream.listen((_) => setupTTS());
} }
bool get _useAlternativeTTS { bool get _useAlternativeTTS {
return PlatformInfos.getOperatingSystem() == 'Windows'; return PlatformInfos.isWindows;
} }
Future<void> dispose() async { Future<void> dispose() async {
await _tts.stop(); await _tts.stop();
await _languageSubscription?.cancel();
} }
void _onError(dynamic message) => ErrorHandler.logError( void _onError(dynamic message) => ErrorHandler.logError(
@ -40,39 +49,46 @@ class TtsController {
); );
Future<void> setupTTS() async { Future<void> setupTTS() async {
if (_useAlternativeTTS) {
await _setupAltTTS();
return;
}
try { try {
_tts.setErrorHandler(_onError); if (_useAlternativeTTS) {
debugger(when: kDebugMode && targetLanguage == null); await _setupAltTTS();
} else {
_tts.setLanguage( _tts.setErrorHandler(_onError);
targetLanguage ?? "en", debugger(when: kDebugMode && targetLanguage == null);
);
_tts.setLanguage(
await _tts.awaitSpeakCompletion(true); targetLanguage ?? "en",
);
final voices = (await _tts.getVoices) as List?;
_availableLangCodes = (voices ?? [])
.map((v) {
// on iOS / web, the codes are in 'locale', but on Android, they are in 'name'
final nameCode = v['name']?.split("-").first;
final localeCode = v['locale']?.split("-").first;
return nameCode.length == 2 ? nameCode : localeCode;
})
.toSet()
.cast<String>()
.toList();
debugPrint("availableLangCodes: $_availableLangCodes");
debugger(when: kDebugMode && !_isLanguageFullySupported); await _tts.awaitSpeakCompletion(true);
final voices = (await _tts.getVoices) as List?;
_availableLangCodes = (voices ?? [])
.map((v) {
// on iOS / web, the codes are in 'locale', but on Android, they are in 'name'
final nameCode = v['name']?.split("-").first;
final localeCode = v['locale']?.split("-").first;
return nameCode.length == 2 ? nameCode : localeCode;
})
.toSet()
.cast<String>()
.toList();
}
} catch (e, s) { } catch (e, s) {
debugger(when: kDebugMode); debugger(when: kDebugMode);
ErrorHandler.logError(e: e, s: s); ErrorHandler.logError(e: e, s: s);
} finally {
debugPrint("availableLangCodes: $_availableLangCodes");
final enableTTSSetting = userController.profile.toolSettings.enableTTS;
if (enableTTSSetting != isLanguageFullySupported) {
await userController.updateProfile(
(profile) {
profile.toolSettings.enableTTS = isLanguageFullySupported;
return profile;
},
waitForDataInSync: true,
);
}
} }
} }
@ -148,7 +164,12 @@ class TtsController {
// TODO - make non-nullable again // TODO - make non-nullable again
String? eventID, String? eventID,
) async { ) async {
if (_isLanguageFullySupported) { if (!MatrixState
.pangeaController.userController.profile.toolSettings.enableTTS) {
return;
}
if (isLanguageFullySupported) {
await _speak(text); await _speak(text);
} else { } else {
ErrorHandler.logError( ErrorHandler.logError(
@ -200,6 +221,6 @@ class TtsController {
} }
} }
bool get _isLanguageFullySupported => bool get isLanguageFullySupported =>
_availableLangCodes.contains(targetLanguage); _availableLangCodes.contains(targetLanguage);
} }

@ -7,12 +7,14 @@ class ProfileSettingsSwitchListTile extends StatefulWidget {
final String title; final String title;
final String? subtitle; final String? subtitle;
final Function(bool) onChange; final Function(bool) onChange;
final bool enabled;
const ProfileSettingsSwitchListTile.adaptive({ const ProfileSettingsSwitchListTile.adaptive({
super.key, super.key,
required this.defaultValue, required this.defaultValue,
required this.title, required this.title,
required this.onChange, required this.onChange,
this.enabled = true,
this.subtitle, this.subtitle,
}); });
@ -30,6 +32,14 @@ class PSettingsSwitchListTileState
super.initState(); super.initState();
} }
@override
void didUpdateWidget(ProfileSettingsSwitchListTile oldWidget) {
if (oldWidget.defaultValue != widget.defaultValue) {
setState(() => currentValue = widget.defaultValue);
}
super.didUpdateWidget(oldWidget);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SwitchListTile.adaptive( return SwitchListTile.adaptive(
@ -37,18 +47,20 @@ class PSettingsSwitchListTileState
title: Text(widget.title), title: Text(widget.title),
activeColor: AppConfig.activeToggleColor, activeColor: AppConfig.activeToggleColor,
subtitle: widget.subtitle != null ? Text(widget.subtitle!) : null, subtitle: widget.subtitle != null ? Text(widget.subtitle!) : null,
onChanged: (bool newValue) async { onChanged: widget.enabled
try { ? (bool newValue) async {
widget.onChange(newValue); try {
setState(() => currentValue = newValue); widget.onChange(newValue);
} catch (err, s) { setState(() => currentValue = newValue);
ErrorHandler.logError( } catch (err, s) {
e: err, ErrorHandler.logError(
m: "Failed to updates user setting", e: err,
s: s, m: "Failed to updates user setting",
); s: s,
} );
}, }
}
: null,
); );
} }
} }

@ -13,7 +13,10 @@ import '../config/app_config.dart';
abstract class PlatformInfos { abstract class PlatformInfos {
static bool get isWeb => kIsWeb; static bool get isWeb => kIsWeb;
static bool get isLinux => !kIsWeb && Platform.isLinux; static bool get isLinux => !kIsWeb && Platform.isLinux;
static bool get isWindows => !kIsWeb && Platform.isWindows; // #Pangea
// static bool get isWindows => !kIsWeb && Platform.isWindows;
static bool get isWindows => getOperatingSystem() == 'Windows';
// Pangea#
static bool get isMacOS => !kIsWeb && Platform.isMacOS; static bool get isMacOS => !kIsWeb && Platform.isMacOS;
static bool get isIOS => !kIsWeb && Platform.isIOS; static bool get isIOS => !kIsWeb && Platform.isIOS;
static bool get isAndroid => !kIsWeb && Platform.isAndroid; static bool get isAndroid => !kIsWeb && Platform.isAndroid;

Loading…
Cancel
Save