make missing voice warning into an instructions popup

pull/1476/head
ggurdin 1 year ago
parent fe41800e05
commit ea1ad9bc61
No known key found for this signature in database
GPG Key ID: A01CB41737CBB478

@ -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",

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

@ -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,
),
);
}

@ -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;
}
}
}

@ -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;
}

@ -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),
],
),

@ -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",

@ -2,20 +2,17 @@ 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({
required this.targetLangCode,
super.key,
});
const MissingVoiceButton({super.key});
Future<void> launchTTSSettings(BuildContext context) async {
if (Platform.isAndroid) {
if (!kIsWeb && Platform.isAndroid) {
const intent = AndroidIntent(
action: 'com.android.settings.TTS_SETTINGS',
package: 'com.talktolearn.chat',
@ -30,36 +27,18 @@ class MissingVoiceButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
constraints: const BoxConstraints(maxWidth: AppConfig.toolbarMinWidth),
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(context),
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),
),
);
}

@ -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!,
);
}

@ -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,

@ -302,6 +302,7 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
practiceCardController: this,
currentActivity: currentActivity!,
tts: widget.tts,
eventID: widget.pangeaMessageEvent.eventId,
);
case ActivityTypeEnum.wordFocusListening:
// return WordFocusListeningActivity(
@ -310,6 +311,7 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
practiceCardController: this,
currentActivity: currentActivity!,
tts: widget.tts,
eventID: widget.pangeaMessageEvent.eventId,
);
// default:
// ErrorHandler.logError(

@ -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
);
}
}

Loading…
Cancel
Save