Merge pull request #909 from pangeachat/890-address-case-of-tts-not-being-available-on-certain-devices

890 address case of tts not being available on certain devices
This commit is contained in:
ggurdin 2024-11-04 16:03:35 -05:00 committed by GitHub
commit 85266463f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 155 additions and 103 deletions

View file

@ -4215,8 +4215,9 @@
"l2SupportAlpha": "Alpha",
"l2SupportBeta": "Beta",
"l2SupportFull": "Full",
"voiceNotAvailable": "It looks like you don't have a voice installed for this language.",
"openVoiceSettings": "Click here to open voice settings",
"missingVoiceTitle": "Missing voice",
"voiceNotAvailable": "You don't have a voice installed for this language.",
"openVoiceSettings": "Open voice settings",
"playAudio": "Play",
"stop": "Stop",
"grammarCopySCONJ": "Subordinating Conjunction",

View file

@ -56,7 +56,6 @@ class ChatEventList extends StatelessWidget {
context,
InstructionsEnum.clickMessage,
msgEvents[0].eventId,
true,
);
});
// Pangea#

View file

@ -41,7 +41,6 @@ class ITBotButton extends StatelessWidget {
context,
InstructionsEnum.itInstructions,
choreographer.itBotTransformTargetKey,
true,
);
return IconButton(
@ -51,7 +50,7 @@ class ITBotButton extends StatelessWidget {
context,
InstructionsEnum.itInstructions,
choreographer.itBotTransformTargetKey,
false,
showToggle: false,
),
);
}

View file

@ -15,6 +15,7 @@ enum InstructionsEnum {
l1Translation,
translationChoices,
clickAgainToDeselect,
missingVoice,
}
extension InstructionsEnumExtension on InstructionsEnum {
@ -28,6 +29,8 @@ extension InstructionsEnumExtension on InstructionsEnum {
return l10n.blurMeansTranslateTitle;
case InstructionsEnum.tooltipInstructions:
return l10n.tooltipInstructionsTitle;
case InstructionsEnum.missingVoice:
return l10n.missingVoiceTitle;
case InstructionsEnum.clickAgainToDeselect:
case InstructionsEnum.speechToText:
case InstructionsEnum.l1Translation:
@ -64,6 +67,8 @@ extension InstructionsEnumExtension on InstructionsEnum {
return PlatformInfos.isMobile
? l10n.tooltipInstructionsMobileBody
: l10n.tooltipInstructionsBrowserBody;
case InstructionsEnum.missingVoice:
return l10n.voiceNotAvailable;
}
}
@ -87,6 +92,8 @@ extension InstructionsEnumExtension on InstructionsEnum {
return instructionSettings.showedTranslationChoicesTooltip;
case InstructionsEnum.clickAgainToDeselect:
return instructionSettings.showedClickAgainToDeselect;
case InstructionsEnum.missingVoice:
return instructionSettings.showedMissingVoice;
}
}
}

View file

@ -195,6 +195,8 @@ class MessageActivityRequest {
final String messageId;
final List<ActivityTypeEnum> clientCompatibleActivities;
MessageActivityRequest({
required this.userL1,
required this.userL2,
@ -203,9 +205,28 @@ class MessageActivityRequest {
required this.messageId,
required this.existingActivities,
required this.activityQualityFeedback,
});
clientCompatibleActivities,
}) : clientCompatibleActivities =
clientCompatibleActivities ?? ActivityTypeEnum.values;
factory MessageActivityRequest.fromJson(Map<String, dynamic> json) {
final clientCompatibleActivitiesEntry =
json['client_version_compatible_activity_types'];
List<ActivityTypeEnum>? clientCompatibleActivities;
if (clientCompatibleActivitiesEntry != null &&
clientCompatibleActivitiesEntry is List) {
clientCompatibleActivities = clientCompatibleActivitiesEntry
.map(
(e) => ActivityTypeEnum.values.firstWhereOrNull(
(element) =>
element.string == e as String ||
element.string.split('.').last == e,
),
)
.where((entry) => entry != null)
.cast<ActivityTypeEnum>()
.toList();
}
return MessageActivityRequest(
userL1: json['user_l1'] as String,
userL2: json['user_l2'] as String,
@ -224,6 +245,10 @@ class MessageActivityRequest {
json['activity_quality_feedback'] as Map<String, dynamic>,
)
: null,
clientCompatibleActivities: clientCompatibleActivities != null &&
clientCompatibleActivities.isNotEmpty
? clientCompatibleActivities
: ActivityTypeEnum.values,
);
}
@ -241,7 +266,7 @@ class MessageActivityRequest {
// the server will only return activities of these types
// this for backwards compatibility with old clients
'client_version_compatible_activity_types':
ActivityTypeEnum.values.map((e) => e.string).toList(),
clientCompatibleActivities.map((e) => e.string).toList(),
};
}

View file

@ -185,6 +185,7 @@ class UserInstructions {
bool showedClickMessage;
bool showedBlurMeansTranslate;
bool showedTooltipInstructions;
bool showedMissingVoice;
bool showedSpeechToTextTooltip;
bool showedL1TranslationTooltip;
@ -200,6 +201,7 @@ class UserInstructions {
this.showedL1TranslationTooltip = false,
this.showedTranslationChoicesTooltip = false,
this.showedClickAgainToDeselect = false,
this.showedMissingVoice = false,
});
factory UserInstructions.fromJson(Map<String, dynamic> json) =>
@ -219,6 +221,8 @@ class UserInstructions {
json[InstructionsEnum.speechToText.toString()] ?? false,
showedClickAgainToDeselect:
json[InstructionsEnum.clickAgainToDeselect.toString()] ?? false,
showedMissingVoice:
json[InstructionsEnum.missingVoice.toString()] ?? false,
);
Map<String, dynamic> toJson() {
@ -236,6 +240,7 @@ class UserInstructions {
data[InstructionsEnum.speechToText.toString()] = showedSpeechToTextTooltip;
data[InstructionsEnum.clickAgainToDeselect.toString()] =
showedClickAgainToDeselect;
data[InstructionsEnum.missingVoice.toString()] = showedMissingVoice;
return data;
}

View file

@ -56,6 +56,9 @@ class InstructionsController {
case InstructionsEnum.clickAgainToDeselect:
profile.instructionSettings.showedClickAgainToDeselect = value;
break;
case InstructionsEnum.missingVoice:
profile.instructionSettings.showedMissingVoice = value;
break;
}
return profile;
});
@ -66,9 +69,10 @@ class InstructionsController {
Future<void> showInstructionsPopup(
BuildContext context,
InstructionsEnum key,
String transformTargetKey, [
String transformTargetKey, {
bool showToggle = true,
]) async {
Widget? customContent,
}) async {
final bool userLangsSet =
await _pangeaController.userController.areUserLanguagesSet;
if (!userLangsSet) {
@ -115,6 +119,7 @@ class InstructionsController {
style: botStyle,
),
),
if (customContent != null) customContent,
if (showToggle) InstructionsToggle(instructionsKey: key),
],
),

View file

@ -71,7 +71,11 @@ class MessageAudioCardState extends State<MessageAudioCard> {
final PangeaTokenText selection = widget.selection!;
final tokenText = selection.content;
await widget.tts.speak(tokenText);
await widget.tts.tryToSpeak(
tokenText,
context,
widget.messageEvent.eventId,
);
}
void setSectionStartAndEnd(int? start, int? end) => mounted
@ -196,19 +200,13 @@ class MessageAudioCardState extends State<MessageAudioCard> {
child: _isLoading
? const ToolbarContentLoadingIndicator()
: audioFile != null
? Column(
children: [
AudioPlayerWidget(
null,
matrixFile: audioFile,
sectionStartMS: sectionStartMS,
sectionEndMS: sectionEndMS,
color:
Theme.of(context).colorScheme.onPrimaryContainer,
setIsPlayingAudio: widget.setIsPlayingAudio,
),
widget.tts.missingVoiceButton,
],
? AudioPlayerWidget(
null,
matrixFile: audioFile,
sectionStartMS: sectionStartMS,
sectionEndMS: sectionEndMS,
color: Theme.of(context).colorScheme.onPrimaryContainer,
setIsPlayingAudio: widget.setIsPlayingAudio,
)
: const CardErrorWidget(
error: "Null audio file in message_audio_card",

View file

@ -2,26 +2,23 @@ import 'dart:io';
import 'package:android_intent_plus/android_intent.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
class MissingVoiceButton extends StatelessWidget {
final String targetLangCode;
const MissingVoiceButton({super.key});
const MissingVoiceButton({
required this.targetLangCode,
super.key,
});
void launchTTSSettings(BuildContext context) {
if (Platform.isAndroid) {
Future<void> launchTTSSettings(BuildContext context) async {
if (!kIsWeb && Platform.isAndroid) {
const intent = AndroidIntent(
action: 'com.android.settings.TTS_SETTINGS',
package: 'com.talktolearn.chat',
);
showFutureLoadingDialog(
await showFutureLoadingDialog(
context: context,
future: intent.launch,
);
@ -30,37 +27,18 @@ class MissingVoiceButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.1),
borderRadius: const BorderRadius.all(
Radius.circular(AppConfig.borderRadius),
return TextButton(
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all<Color>(
AppConfig.primaryColor.withOpacity(0.1),
),
),
padding: const EdgeInsets.all(8),
margin: const EdgeInsets.only(top: 8),
child: SizedBox(
width: AppConfig.toolbarMinWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
L10n.of(context)!.voiceNotAvailable,
textAlign: TextAlign.center,
),
TextButton(
onPressed: () => launchTTSSettings,
// commenting out as suspecting this is causing an issue
// #freeze-activity
style: const ButtonStyle(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(L10n.of(context)!.openVoiceSettings),
),
],
),
onPressed: () async {
MatrixState.pAnyState.closeOverlay();
await launchTTSSettings(context);
},
child: Center(
child: Text(L10n.of(context)!.openVoiceSettings),
),
);
}

View file

@ -1,8 +1,8 @@
import 'dart:developer';
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/widgets/chat/missing_voice_button.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -85,6 +85,37 @@ class TtsController {
}
}
Future<void> showMissingVoicePopup(
BuildContext context,
String eventID,
) async {
await MatrixState.pangeaController.instructions.showInstructionsPopup(
context,
InstructionsEnum.missingVoice,
eventID,
showToggle: false,
customContent: const Padding(
padding: EdgeInsets.only(top: 12),
child: MissingVoiceButton(),
),
);
return;
}
/// A safer version of speak, that handles the case of
/// the language not being supported by the TTS engine
Future<void> tryToSpeak(
String text,
BuildContext context,
String eventID,
) async {
if (isLanguageFullySupported) {
await speak(text);
} else {
await showMissingVoicePopup(context, eventID);
}
}
Future<void> speak(String text) async {
try {
stop();
@ -112,11 +143,4 @@ class TtsController {
bool get isLanguageFullySupported =>
availableLangCodes.contains(targetLanguage);
Widget get missingVoiceButton => targetLanguage != null &&
(kIsWeb || isLanguageFullySupported || !PlatformInfos.isAndroid)
? const SizedBox.shrink()
: MissingVoiceButton(
targetLangCode: targetLanguage!,
);
}

View file

@ -18,12 +18,14 @@ class MultipleChoiceActivity extends StatefulWidget {
final PracticeActivityCardState practiceCardController;
final PracticeActivityModel currentActivity;
final TtsController tts;
final String eventID;
const MultipleChoiceActivity({
super.key,
required this.practiceCardController,
required this.currentActivity,
required this.tts,
required this.eventID,
});
@override
@ -117,6 +119,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
WordAudioButton(
text: practiceActivity.content.answer,
ttsController: widget.tts,
eventID: widget.eventID,
),
ChoicesArray(
isLoading: false,

View file

@ -148,6 +148,11 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
.map((activity) => activity.activityRequestMetaData)
.toList(),
activityQualityFeedback: activityFeedback,
clientCompatibleActivities: widget.tts.isLanguageFullySupported
? ActivityTypeEnum.values
: ActivityTypeEnum.values
.where((type) => type != ActivityTypeEnum.wordFocusListening)
.toList(),
),
widget.pangeaMessageEvent,
);
@ -297,6 +302,7 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
practiceCardController: this,
currentActivity: currentActivity!,
tts: widget.tts,
eventID: widget.pangeaMessageEvent.eventId,
);
case ActivityTypeEnum.wordFocusListening:
// return WordFocusListeningActivity(
@ -305,6 +311,7 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
practiceCardController: this,
currentActivity: currentActivity!,
tts: widget.tts,
eventID: widget.pangeaMessageEvent.eventId,
);
// default:
// ErrorHandler.logError(

View file

@ -5,11 +5,13 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
class WordAudioButton extends StatefulWidget {
final String text;
final TtsController ttsController;
final String eventID;
const WordAudioButton({
super.key,
required this.text,
required this.ttsController,
required this.eventID,
});
@override
@ -22,41 +24,40 @@ class WordAudioButtonState extends State<WordAudioButton> {
@override
Widget build(BuildContext context) {
debugPrint('build WordAudioButton');
return Column(
children: [
IconButton(
icon: const Icon(Icons.play_arrow_outlined),
isSelected: _isPlaying,
selectedIcon: const Icon(Icons.pause_outlined),
color: _isPlaying ? Colors.white : null,
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
_isPlaying
? Theme.of(context).colorScheme.secondary
: Theme.of(context).colorScheme.primaryContainer,
),
),
tooltip:
_isPlaying ? L10n.of(context)!.stop : L10n.of(context)!.playAudio,
onPressed: () async {
if (_isPlaying) {
await widget.ttsController.tts.stop();
if (mounted) {
setState(() => _isPlaying = false);
}
} else {
if (mounted) {
setState(() => _isPlaying = true);
}
await widget.ttsController.speak(widget.text);
if (mounted) {
setState(() => _isPlaying = false);
}
}
}, // Disable button if language isn't supported
return IconButton(
icon: const Icon(Icons.play_arrow_outlined),
isSelected: _isPlaying,
selectedIcon: const Icon(Icons.pause_outlined),
color: _isPlaying ? Colors.white : null,
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
_isPlaying
? Theme.of(context).colorScheme.secondary
: Theme.of(context).colorScheme.primaryContainer,
),
widget.ttsController.missingVoiceButton,
],
),
tooltip:
_isPlaying ? L10n.of(context)!.stop : L10n.of(context)!.playAudio,
onPressed: () async {
if (_isPlaying) {
await widget.ttsController.tts.stop();
if (mounted) {
setState(() => _isPlaying = false);
}
} else {
if (mounted) {
setState(() => _isPlaying = true);
}
await widget.ttsController.tryToSpeak(
widget.text,
context,
widget.eventID,
);
if (mounted) {
setState(() => _isPlaying = false);
}
}
}, // Disable button if language isn't supported
);
}
}