make missing voice warning into an instructions popup
This commit is contained in:
parent
fe41800e05
commit
ea1ad9bc61
12 changed files with 121 additions and 98 deletions
|
|
@ -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…
Add table
Reference in a new issue