Merge branch 'main' into 896-group-constructs-according-to-categories
This commit is contained in:
commit
17e295b168
17 changed files with 235 additions and 129 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",
|
||||
"grammarCopyPOSsconj": "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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import 'package:matrix/matrix.dart';
|
|||
/// Represents an item in the completion cache.
|
||||
class _RequestCacheItem {
|
||||
MessageActivityRequest req;
|
||||
PracticeActivityModel? practiceActivity;
|
||||
PracticeActivityModelResponse? practiceActivity;
|
||||
|
||||
_RequestCacheItem({
|
||||
required this.req,
|
||||
|
|
@ -99,7 +99,7 @@ class PracticeGenerationController {
|
|||
|
||||
//TODO - allow return of activity content before sending the event
|
||||
// this requires some downstream changes to the way the event is handled
|
||||
Future<PracticeActivityModel?> getPracticeActivity(
|
||||
Future<PracticeActivityModelResponse?> getPracticeActivity(
|
||||
MessageActivityRequest req,
|
||||
PangeaMessageEvent event,
|
||||
) async {
|
||||
|
|
@ -119,6 +119,8 @@ class PracticeGenerationController {
|
|||
return null;
|
||||
}
|
||||
|
||||
final eventCompleter = Completer<PracticeActivityEvent?>();
|
||||
|
||||
// if the server points to an existing event, return that event
|
||||
if (res.existingActivityEventId != null) {
|
||||
final Event? existingEvent =
|
||||
|
|
@ -127,11 +129,19 @@ class PracticeGenerationController {
|
|||
debugPrint(
|
||||
'Existing activity event found: ${existingEvent?.content}',
|
||||
);
|
||||
if (existingEvent != null) {
|
||||
return PracticeActivityEvent(
|
||||
debugPrint(
|
||||
"eventID: ${existingEvent?.eventId}, event is redacted: ${existingEvent?.redacted}",
|
||||
);
|
||||
if (existingEvent != null && !existingEvent.redacted) {
|
||||
final activityEvent = PracticeActivityEvent(
|
||||
event: existingEvent,
|
||||
timeline: event.timeline,
|
||||
).practiceActivity;
|
||||
);
|
||||
eventCompleter.complete(activityEvent);
|
||||
return PracticeActivityModelResponse(
|
||||
activity: activityEvent.practiceActivity,
|
||||
eventCompleter: eventCompleter,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -141,11 +151,30 @@ class PracticeGenerationController {
|
|||
}
|
||||
|
||||
debugPrint('Activity generated: ${res.activity!.toJson()}');
|
||||
_sendAndPackageEvent(res.activity!, event).then((event) {
|
||||
eventCompleter.complete(event);
|
||||
});
|
||||
|
||||
_sendAndPackageEvent(res.activity!, event);
|
||||
_cache[cacheKey] =
|
||||
_RequestCacheItem(req: req, practiceActivity: res.activity!);
|
||||
final responseModel = PracticeActivityModelResponse(
|
||||
activity: res.activity!,
|
||||
eventCompleter: eventCompleter,
|
||||
);
|
||||
|
||||
return _cache[cacheKey]!.practiceActivity;
|
||||
_cache[cacheKey] = _RequestCacheItem(
|
||||
req: req,
|
||||
practiceActivity: responseModel,
|
||||
);
|
||||
|
||||
return responseModel;
|
||||
}
|
||||
}
|
||||
|
||||
class PracticeActivityModelResponse {
|
||||
final PracticeActivityModel? activity;
|
||||
final Completer<PracticeActivityEvent?> eventCompleter;
|
||||
|
||||
PracticeActivityModelResponse({
|
||||
required this.activity,
|
||||
required this.eventCompleter,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -47,14 +47,6 @@ class MessageToolbar extends StatelessWidget {
|
|||
final bool messageInUserL2 = pangeaMessageEvent.messageDisplayLangCode ==
|
||||
MatrixState.pangeaController.languageController.userL2?.langCode;
|
||||
|
||||
// If not in the target language show specific messsage
|
||||
if (!messageInUserL2) {
|
||||
return MessageDisplayCard(
|
||||
displayText:
|
||||
L10n.of(context)!.messageNotInTargetLang, // Pass the display text,
|
||||
);
|
||||
}
|
||||
|
||||
switch (overLayController.toolbarMode) {
|
||||
case MessageMode.translation:
|
||||
return MessageTranslationCard(
|
||||
|
|
@ -104,6 +96,13 @@ class MessageToolbar extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
case MessageMode.practiceActivity:
|
||||
// If not in the target language show specific messsage
|
||||
if (!messageInUserL2) {
|
||||
return MessageDisplayCard(
|
||||
displayText: L10n.of(context)!
|
||||
.messageNotInTargetLang, // Pass the display text,
|
||||
);
|
||||
}
|
||||
return PracticeActivityCard(
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
overlayController: overLayController,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:fluffychat/config/themes.dart';
|
|||
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ToolbarButtons extends StatelessWidget {
|
||||
|
|
@ -25,10 +26,16 @@ class ToolbarButtons extends StatelessWidget {
|
|||
.where((mode) => mode.shouldShowAsToolbarButton(pangeaMessageEvent.event))
|
||||
.toList();
|
||||
|
||||
bool get messageInUserL2 =>
|
||||
pangeaMessageEvent.messageDisplayLangCode ==
|
||||
MatrixState.pangeaController.languageController.userL2?.langCode;
|
||||
|
||||
static const double iconWidth = 36.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final totallyDone =
|
||||
overlayController.isPracticeComplete || !messageInUserL2;
|
||||
final double barWidth = width - iconWidth;
|
||||
|
||||
if (overlayController.pangeaMessageEvent.isAudioMessage) {
|
||||
|
|
@ -85,14 +92,14 @@ class ToolbarButtons extends StatelessWidget {
|
|||
index,
|
||||
overlayController.toolbarMode,
|
||||
pangeaMessageEvent.numberOfActivitiesCompleted,
|
||||
overlayController.isPracticeComplete,
|
||||
totallyDone,
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: mode.isUnlocked(
|
||||
index,
|
||||
pangeaMessageEvent.numberOfActivitiesCompleted,
|
||||
overlayController.isPracticeComplete,
|
||||
totallyDone,
|
||||
)
|
||||
? () => overlayController.updateToolbarMode(mode)
|
||||
: null,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'dart:developer';
|
|||
|
||||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/practice_activity_generation_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
|
|
@ -45,6 +46,8 @@ class PracticeActivityCard extends StatefulWidget {
|
|||
|
||||
class PracticeActivityCardState extends State<PracticeActivityCard> {
|
||||
PracticeActivityModel? currentActivity;
|
||||
Completer<PracticeActivityEvent?>? currentActivityCompleter;
|
||||
|
||||
PracticeActivityRecordModel? currentCompletionRecord;
|
||||
bool fetchingActivity = false;
|
||||
|
||||
|
|
@ -133,9 +136,9 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
return null;
|
||||
}
|
||||
|
||||
final PracticeActivityModel? ourNewActivity = await pangeaController
|
||||
.practiceGenerationController
|
||||
.getPracticeActivity(
|
||||
final PracticeActivityModelResponse? activityResponse =
|
||||
await pangeaController.practiceGenerationController
|
||||
.getPracticeActivity(
|
||||
MessageActivityRequest(
|
||||
userL1: pangeaController.languageController.userL1!.langCode,
|
||||
userL2: pangeaController.languageController.userL2!.langCode,
|
||||
|
|
@ -148,13 +151,19 @@ 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,
|
||||
);
|
||||
|
||||
currentActivityCompleter = activityResponse?.eventCompleter;
|
||||
_updateFetchingActivity(false);
|
||||
|
||||
return ourNewActivity;
|
||||
return activityResponse?.activity;
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
|
|
@ -250,12 +259,27 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
|
||||
/// clear the current activity, record, and selection
|
||||
/// fetch a new activity, including the offending activity in the request
|
||||
void submitFeedback(String feedback) {
|
||||
if (currentActivity == null) {
|
||||
Future<void> submitFeedback(String feedback) async {
|
||||
if (currentActivity == null || currentCompletionRecord == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentActivityCompleter != null) {
|
||||
final activityEvent = await currentActivityCompleter!.future;
|
||||
await activityEvent?.event.redactEvent(reason: feedback);
|
||||
} else {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: Exception('No completer found for current activity'),
|
||||
data: {
|
||||
'activity': currentActivity,
|
||||
'record': currentCompletionRecord,
|
||||
'feedback': feedback,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_fetchNewActivity(
|
||||
ActivityQualityFeedback(
|
||||
feedbackText: feedback,
|
||||
|
|
@ -297,6 +321,7 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
practiceCardController: this,
|
||||
currentActivity: currentActivity!,
|
||||
tts: widget.tts,
|
||||
eventID: widget.pangeaMessageEvent.eventId,
|
||||
);
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
// return WordFocusListeningActivity(
|
||||
|
|
@ -305,6 +330,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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ description: Learn a language while texting your friends.
|
|||
# Pangea#
|
||||
publish_to: none
|
||||
# On version bump also increase the build number for F-Droid
|
||||
version: 1.23.3+3562
|
||||
version: 1.23.4+3563
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue