merge in main branch

This commit is contained in:
ggurdin 2024-10-24 16:42:26 -04:00
commit 358e874ce4
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
65 changed files with 996 additions and 746 deletions

View file

@ -2563,7 +2563,7 @@
"type": "text",
"placeholders": {}
},
"interactiveTranslatorAllowedDesc": "Students can choose whether to use translation assistance in space group chats in Main Menu > My Learning Settings.",
"interactiveTranslatorAllowedDesc": "Students can choose whether to use translation assistance in space group chats in Main Menu > Learning Settings.",
"@interactiveTranslatorAllowedDesc": {
"type": "text",
"placeholders": {}
@ -3030,9 +3030,9 @@
"errorDisableLanguageAssistanceClassDesc": "Translation assistance and grammar assistance are turned off for the space that this chat is in.",
"itIsDisabled": "Interactive Translation is disabled",
"igcIsDisabled": "Interactive Grammar Checking is disabled",
"goToLearningSettings": "Go to My Learning Settings",
"goToLearningSettings": "Go to Learning Settings",
"error405Title": "Languages not set",
"error405Desc": "Please set your languages in Main Menu > My Learning Settings.",
"error405Desc": "Please set your languages in Main Menu > Learning Settings.",
"loginOrSignup": "Sign in with",
"@loginOrSignup": {
"type": "text",
@ -3095,7 +3095,7 @@
"type": "text",
"placeholders": {}
},
"learningSettings": "My Learning Settings",
"learningSettings": "Learning settings",
"classNameRequired": "Please enter a space name",
"@classNameRequired": {
"type": "text",
@ -4016,9 +4016,9 @@
"conversationBotModeSelectOption_storyGame": "Story Game",
"conversationBotDiscussionZone_title": "Discussion Settings",
"conversationBotDiscussionZone_discussionTopicLabel": "Discussion Topic",
"conversationBotDiscussionZone_discussionTopicPlaceholder": "Set Discussion Topic",
"conversationBotDiscussionZone_discussionTopicPlaceholder": "Set discussion topic",
"conversationBotDiscussionZone_discussionKeywordsLabel": "Discussion Keywords",
"conversationBotDiscussionZone_discussionKeywordsPlaceholder": "Set Discussion Keywords",
"conversationBotDiscussionZone_discussionKeywordsPlaceholder": "Set discussion keywords",
"conversationBotDiscussionZone_discussionKeywordsHintText": "Comma separated list of keywords to guide the discussion",
"conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel": "Send discussion prompt on a schedule",
"conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel": "Hours between discussion prompts",
@ -4364,5 +4364,6 @@
"selectBotLanguage": "Select bot language",
"chooseVoice": "Choose a voice",
"enterLanguageLevel": "Please enter a language level",
"enterDiscussionTopic": "Please enter a discussion topic"
"enterDiscussionTopic": "Please enter a discussion topic",
"selectBotChatMode": "Select chat mode"
}

View file

@ -4731,5 +4731,145 @@
}
},
"commandHint_googly": "Enviar unos ojos saltones",
"reportContentIssue": "Problema de contenido"
"reportContentIssue": "Problema de contenido",
"alwaysUse24HourFormat": "falso",
"countChatsAndCountParticipants": "{chats} chats y {participants} participantes",
"@countChatsAndCountParticipants": {
"type": "text",
"placeholders": {
"chats": {},
"participants": {}
}
},
"noMoreChatsFound": "No se encontraron más chats...",
"noChatsFoundHere": "Aún no se encontraron chats aquí. Inicia un nuevo chat con alguien usando el botón de abajo. ⤵️",
"joinedChats": "Chats unidos",
"unread": "No leído",
"space": "Espacio",
"spaces": "Espacios",
"enterASpacepName": "Ingresa un nombre",
"invitedBy": "📩 Invitado por {user}",
"@invitedBy": {
"placeholders": {
"user": {}
}
},
"clickMessageBody": "Haz clic en un mensaje para herramientas de idioma como traducción, reproducción y más!",
"searchIn": "Buscar en el chat \"{chat}\"...",
"@searchIn": {
"type": "text",
"placeholders": {
"chat": {}
}
},
"subscribedToUnlockTools": "¡Suscríbete para desbloquear la traducción interactiva y la verificación gramatical, la reproducción de audio, las actividades de práctica personalizadas y la analítica de aprendizaje!",
"conversationBotModeSelectOption_storyGame": "Juego de Historia",
"conversationBotCustomZone_title": "Configuraciones Personalizadas",
"conversationBotCustomZone_customSystemPromptLabel": "Mensaje del sistema",
"conversationBotCustomZone_customSystemPromptPlaceholder": "Establecer mensaje del sistema personalizado",
"conversationBotCustomZone_customSystemPromptEmptyError": "Falta mensaje del sistema personalizado",
"conversationBotCustomZone_customTriggerReactionEnabledLabel": "Responde a la reacción ⏩",
"botConfig": "Configuraciones del Bot de Conversación",
"addConversationBotDialogTitleInvite": "Confirmar la invitación del bot de conversación",
"addConversationBotButtonInvite": "Invitar",
"addConversationBotDialogInviteConfirmation": "Invitar",
"addConversationBotButtonTitleRemove": "Confirmar la eliminación del bot de conversación",
"addConversationBotButtonRemove": "Eliminar",
"addConversationBotDialogRemoveConfirmation": "Eliminar",
"conversationBotConfigConfirmChange": "Confirmar",
"conversationBotStatus": "Estado del Bot",
"conversationBotTextAdventureZone_title": "Aventura de Texto",
"conversationBotTextAdventureZone_instructionLabel": "Instrucciones del Maestro del Juego",
"conversationBotTextAdventureZone_instructionPlaceholder": "Establecer instrucciones del maestro del juego",
"conversationBotCustomZone_instructionSystemPromptEmptyError": "Faltan instrucciones del maestro del juego",
"suggestToSpace": "Sugerir este espacio",
"suggestToSpaceDesc": "Los subespacios sugeridos aparecerán en la lista de chats de su espacio principal",
"practice": "Práctica",
"noLanguagesSet": "No hay idiomas configurados",
"hintTitle": "Sugerencia:",
"speechToTextBody": "Ve qué tan bien lo hiciste al mirar tus puntajes de Precisión y Palabras Por Minuto.",
"previous": "Anterior",
"languageButtonLabel": "Idioma: {currentLanguage}",
"@languageButtonLabel": {
"type": "text",
"placeholders": {
"currentLanguage": {}
}
},
"changeAnalyticsView": "Cambiar Vista de Análisis",
"l1TranslationBody": "Los mensajes en tu idioma base no serán traducidos.",
"continueText": "Continuar",
"deleteSubscriptionWarningTitle": "YTienes una suscripción activa",
"deleteSubscriptionWarningBody": "Eliminar tu cuenta no cancelará automáticamente tu suscripción.",
"manageSubscription": "Gestionar Suscripción",
"createSpace": "Crear espacio",
"createChat": "Crear chat",
"error520Title": "Por favor, intenta de nuevo.",
"error520Desc": "Lo sentimos, no pudimos entender tu mensaje...",
"wordsUsed": "Palabras Usadas",
"errorTypes": "Tipos de Error",
"level": "Nivel",
"canceledSend": "Envío cancelado",
"morphsUsed": "Morphs Usados",
"translationChoicesBody": "Haz clic y mantén presionada una opción para una pista.",
"sendCanceled": "Envío cancelado",
"goToSpace": "Ir al espacio: {space}",
"@goToSpace": {
"type": "text",
"space": {}
},
"markAsUnread": "Marcar como no leído",
"userLevel": "{level} - Usuario",
"@userLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"moderatorLevel": "{level} - Moderador",
"@moderatorLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"adminLevel": "{level} - Administrador",
"@adminLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"changeGeneralChatSettings": "Cambiar la configuración general del chat.",
"inviteOtherUsers": "Invitar a otros usuarios a este chat",
"changeTheChatPermissions": "Cambiar los permisos del chat",
"changeTheVisibilityOfChatHistory": "Cambiar la visibilidad del historial de chat",
"changeTheCanonicalRoomAlias": "Cambiar la dirección del chat público principal.",
"sendRoomNotifications": "Enviar una notificación a @room",
"changeTheDescriptionOfTheGroup": "Cambiar la descripción del chat",
"chatPermissionsDescription": "Define qué nivel de poder es necesario para ciertas acciones en este chat. Los niveles de poder 0, 50 y 100 suelen representar a usuarios, moderadores y administradores, pero cualquier graduación es posible.",
"updateInstalled": "🎉 ¡Actualización {version} instalada!",
"@updateInstalled": {
"type": "text",
"placeholders": {
"version": {}
}
},
"loginWithMatrixId": "Iniciar sesión con Matrix-ID.",
"discoverHomeservers": "Descubrir homeservers",
"whatIsAHomeserver": "¿Qué es un homeserver?",
"homeserverDescription": "Todos tus datos se almacenan en el homeserver, al igual que un proveedor de correo electrónico. Puedes elegir qué homeserver deseas utilizar, mientras que aún puedes comunicarte con todos. Aprende más en https://matrix.org.",
"doesNotSeemToBeAValidHomeserver": "No parece ser un homeserver compatible. ¿URL incorrecta?",
"grammar": "Gramática",
"contactHasBeenInvitedToTheChat": "El contacto ha sido invitado al chat",
"inviteChat": "📨 Invitar al chat",
"chatName": "Nombre del chat",
"reportContentIssueTitle": "Informar sobre un problema de contenido",
"feedback": "Comentarios opcionales",
"reportContentIssueDescription": "¡Ups! La IA puede facilitar experiencias de aprendizaje personalizadas, pero... también alucina. Por favor, proporciona cualquier comentario que tengas y lo intentaremos de nuevo.",
"clickTheWordAgainToDeselect": "Click the selected word to deselect it.",
"l2SupportNa": "Haz clic en la palabra seleccionada para deseleccionarla",
"l2SupportAlpha": "Alfa",
"l2SupportBeta": "Beta",
"l2SupportFull": "Lleno"
}

View file

@ -23,6 +23,8 @@ abstract class AppConfig {
static const bool allowOtherHomeservers = true;
static const bool enableRegistration = true;
static const double toolbarMaxHeight = 300.0;
static const double toolbarMinHeight = 70.0;
static const double toolbarMinWidth = 270.0;
// #Pangea
// static const Color primaryColor = Color(0xFF5625BA);
// static const Color primaryColorLight = Color(0xFFCCBDEA);

View file

@ -29,7 +29,6 @@ import 'package:fluffychat/pages/settings_style/settings_style.dart';
import 'package:fluffychat/pangea/guard/p_vguard.dart';
import 'package:fluffychat/pangea/pages/find_partner/find_partner.dart';
import 'package:fluffychat/pangea/pages/p_user_age/p_user_age.dart';
import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart';
import 'package:fluffychat/pangea/pages/settings_subscription/settings_subscription.dart';
import 'package:fluffychat/pangea/pages/sign_up/signup.dart';
import 'package:fluffychat/pangea/widgets/class/join_with_link.dart';
@ -406,15 +405,6 @@ abstract class AppRoutes {
],
),
// #Pangea
GoRoute(
path: 'learning',
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const SettingsLearning(),
),
redirect: loggedOutRedirect,
),
GoRoute(
path: 'subscription',
pageBuilder: (context, state) => defaultPageBuilder(

View file

@ -485,6 +485,14 @@ class ChatController extends State<ChatPageWithRoom>
Future<void>? setReadMarkerFuture;
void setReadMarker({String? eventId}) {
// #Pangea
if (eventId != null &&
(eventId.contains("web") ||
eventId.contains("android") ||
eventId.contains("ios"))) {
return;
}
// Pangea#
if (setReadMarkerFuture != null) return;
if (_scrolledUp) return;
if (scrollUpBannerEventId != null) return;

View file

@ -21,6 +21,7 @@ class AudioPlayerWidget extends StatefulWidget {
final Event? event;
final PangeaAudioFile? matrixFile;
final bool autoplay;
final Function(bool)? setIsPlayingAudio;
// Pangea#
static String? currentId;
@ -41,6 +42,7 @@ class AudioPlayerWidget extends StatefulWidget {
this.autoplay = false,
this.sectionStartMS,
this.sectionEndMS,
this.setIsPlayingAudio,
// Pangea#
super.key,
});
@ -204,8 +206,13 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
if (max == null || max == Duration.zero) return;
setState(() => maxPosition = max.inMilliseconds.toDouble());
});
onPlayerStateChanged ??=
audioPlayer.playingStream.listen((_) => setState(() {}));
onPlayerStateChanged ??= audioPlayer.playingStream.listen(
(isPlaying) => setState(() {
// #Pangea
widget.setIsPlayingAudio?.call(isPlaying);
// Pangea#
}),
);
final audioFile = this.audioFile;
if (audioFile != null) {
audioPlayer.setFilePath(audioFile.path);
@ -467,7 +474,7 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
borderRadius: BorderRadius.circular(2),
),
height: 32 * (waveform[i] / 1024),
width: 1.5,
width: 3,
),
],
),

View file

@ -123,7 +123,6 @@ class MessageContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
// debugger(when: overlayController != null);
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
final buttonTextColor = textColor;
switch (event.type) {
@ -307,7 +306,6 @@ class MessageContent extends StatelessWidget {
height: 1.3,
);
// debugger(when: overlayController != null);
if (overlayController != null && pangeaMessageEvent != null) {
return OverlayMessageText(
pangeaMessageEvent: pangeaMessageEvent!,

View file

@ -1,4 +1,5 @@
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart';
import 'package:fluffychat/pangea/utils/logout.dart';
import 'package:fluffychat/pangea/utils/space_code.dart';
import 'package:fluffychat/widgets/avatar.dart';
@ -41,30 +42,17 @@ class ClientChooserButton extends StatelessWidget {
],
),
),
PopupMenuItem(
value: SettingsAction.learning,
child: Row(
children: [
const Icon(Icons.psychology_outlined),
const SizedBox(width: 18),
Expanded(child: Text(L10n.of(context)!.learningSettings)),
],
),
),
// PopupMenuItem(
// value: SettingsAction.newGroup,
// child: Row(
// children: [
// const Icon(Icons.group_add_outlined),
// const SizedBox(width: 18),
// Text(L10n.of(context)!.createGroup),
// ],
// ),
// ),
// Pangea#
PopupMenuItem(
value: SettingsAction.newGroup,
child: Row(
children: [
const Icon(Icons.group_add_outlined),
const SizedBox(width: 18),
// #Pangea
Expanded(child: Text(L10n.of(context)!.createGroup)),
// Text(L10n.of(context)!.createGroup),
// Pangea#
],
),
),
PopupMenuItem(
value: SettingsAction.newSpace,
child: Row(
@ -79,6 +67,16 @@ class ClientChooserButton extends StatelessWidget {
),
),
// #Pangea
PopupMenuItem(
value: SettingsAction.learning,
child: Row(
children: [
const Icon(Icons.psychology_outlined),
const SizedBox(width: 18),
Expanded(child: Text(L10n.of(context)!.learningSettings)),
],
),
),
// PopupMenuItem(
// value: SettingsAction.setStatus,
// child: Row(
@ -306,9 +304,11 @@ class ClientChooserButton extends StatelessWidget {
if (consent != OkCancelResult.ok) return;
context.go('/rooms/settings/addaccount');
break;
case SettingsAction.newGroup:
context.go('/rooms/newgroup');
break;
// #Pangea
// case SettingsAction.newGroup:
// context.go('/rooms/newgroup');
// break;
// Pangea#
case SettingsAction.newSpace:
controller.createNewSpace();
break;
@ -328,7 +328,10 @@ class ClientChooserButton extends StatelessWidget {
// controller.setStatus();
// break;
case SettingsAction.learning:
context.go('/rooms/settings/learning');
showDialog(
context: context,
builder: (c) => const SettingsLearning(),
);
break;
case SettingsAction.joinWithClassCode:
SpaceCodeUtil.joinWithSpaceCodeDialog(
@ -416,7 +419,9 @@ class ClientChooserButton extends StatelessWidget {
enum SettingsAction {
addAccount,
newGroup,
// #Pangea
// newGroup,
// Pangea#
newSpace,
// #Pangea
// setStatus,

View file

@ -317,14 +317,14 @@ class _SpaceViewState extends State<SpaceView> {
key: AddRoomType.subspace,
// #Pangea
// label: L10n.of(context)!.createNewSpace,
label: L10n.of(context)!.newChat,
label: L10n.of(context)!.newSpace,
// Pangea#
),
AlertDialogAction(
key: AddRoomType.chat,
// #Pangea
// label: L10n.of(context)!.createGroup,
label: L10n.of(context)!.createChat,
label: L10n.of(context)!.newChat,
// Pangea#
),
],

View file

@ -24,7 +24,10 @@ class NewGroupView extends StatelessWidget {
onPressed: controller.loading ? null : Navigator.of(context).pop,
),
),
title: Text(L10n.of(context)!.createGroup),
// #Pangea
// title: Text(L10n.of(context)!.createGroup),
title: Text(L10n.of(context)!.newChat),
// Pangea#
),
body: MaxWidthBody(
child: Column(

View file

@ -136,7 +136,8 @@ class NewSpaceController extends State<NewSpace> {
await room.invite(BotName.byEnvironment);
} catch (err) {
ErrorHandler.logError(
e: "Failed to invite pangea bot to space ${room.id}",
e: "Failed to invite pangea bot to new space",
data: {"spaceId": spaceId, "error": err},
);
}
MatrixState.pangeaController.classController

View file

@ -162,11 +162,6 @@ class SettingsView extends StatelessWidget {
title: Text(L10n.of(context)!.subscriptionManagement),
onTap: () => context.go('/rooms/settings/subscription'),
),
ListTile(
leading: const Icon(Icons.psychology_outlined),
title: Text(L10n.of(context)!.learningSettings),
onTap: () => context.go('/rooms/settings/learning'),
),
// Pangea#
ListTile(
leading: const Icon(Icons.shield_outlined),

View file

@ -70,8 +70,8 @@ class Choreographer {
void send(BuildContext context) {
if (isFetching) return;
if (pangeaController.subscriptionController.canSendStatus ==
CanSendStatus.showPaywall) {
if (pangeaController.subscriptionController.subscriptionStatus ==
SubscriptionStatus.showPaywall) {
OverlayUtil.showPositionedCard(
context: context,
cardToShow: PaywallCard(
@ -245,10 +245,10 @@ class Choreographer {
}) async {
try {
if (errorService.isError) return;
final CanSendStatus canSendStatus =
pangeaController.subscriptionController.canSendStatus;
final SubscriptionStatus canSendStatus =
pangeaController.subscriptionController.subscriptionStatus;
if (canSendStatus != CanSendStatus.subscribed ||
if (canSendStatus != SubscriptionStatus.subscribed ||
(!igcEnabled && !itEnabled) ||
(!isAutoIGCEnabled && !manual && choreoMode != ChoreoMode.it)) {
return;

View file

@ -4,7 +4,6 @@ import 'dart:developer';
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
import 'package:fluffychat/pangea/constants/choreo_constants.dart';
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
@ -180,6 +179,18 @@ class ITController {
}
Future<void> getNextTranslationData() async {
if (sourceText == null) {
ErrorHandler.logError(
e: Exception("sourceText is null in getNextTranslationData"),
data: {
"sourceText": sourceText,
"currentITStep": currentITStep,
"nextITStep": nextITStep,
},
);
return;
}
try {
if (completedITSteps.length < goldRouteTracker.continuances.length) {
final String currentText = choreographer.currentText;

View file

@ -44,17 +44,13 @@ class ChoicesArrayState extends State<ChoicesArray> {
void disableInteraction() {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
interactionDisabled = true;
});
if (mounted) setState(() => interactionDisabled = true);
});
}
void enableInteractions() {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
interactionDisabled = false;
});
if (mounted) setState(() => interactionDisabled = false);
});
}

View file

@ -3,12 +3,12 @@ import 'dart:developer';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/models/space_model.dart';
import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import '../../../widgets/matrix.dart';
@ -52,7 +52,12 @@ class LanguagePermissionsButtons extends StatelessWidget {
text: copy.description,
style: const TextStyle(color: AppConfig.primaryColor),
recognizer: TapGestureRecognizer()
..onTap = () => context.go('/rooms/settings/learning'),
..onTap = () {
showDialog(
context: context,
builder: (c) => const SettingsLearning(),
);
},
),
],
),

View file

@ -63,10 +63,13 @@ class StartIGCButtonState extends State<StartIGCButton>
bool get itEnabled => widget.controller.choreographer.itEnabled;
bool get igcEnabled => widget.controller.choreographer.igcEnabled;
CanSendStatus get canSendStatus =>
widget.controller.pangeaController.subscriptionController.canSendStatus;
SubscriptionStatus get subscriptionStatus => widget
.controller.pangeaController.subscriptionController.subscriptionStatus;
bool get grammarCorrectionEnabled =>
(itEnabled || igcEnabled) && canSendStatus == CanSendStatus.subscribed;
(itEnabled || igcEnabled) &&
subscriptionStatus == SubscriptionStatus.subscribed;
@override
Widget build(BuildContext context) {

View file

@ -1,18 +1,18 @@
import 'dart:async';
class BaseController<T> {
final StreamController<T> stateListener = StreamController<T>();
final StreamController<T> _stateListener = StreamController<T>();
late Stream<T> stateStream;
BaseController() {
stateStream = stateListener.stream.asBroadcastStream();
stateStream = _stateListener.stream.asBroadcastStream();
}
dispose() {
stateListener.close();
_stateListener.close();
}
setState(T data) {
stateListener.add(data);
_stateListener.add(data);
}
}

View file

@ -22,7 +22,7 @@ import 'package:sentry_flutter/sentry_flutter.dart';
class GetAnalyticsController {
late PangeaController _pangeaController;
final List<AnalyticsCacheEntry> _cache = [];
StreamSubscription<AnalyticsUpdateType>? _analyticsUpdateSubscription;
StreamSubscription<AnalyticsUpdate>? _analyticsUpdateSubscription;
CachedStreamController<List<OneConstructUse>> analyticsStream =
CachedStreamController<List<OneConstructUse>>();
@ -87,8 +87,9 @@ class GetAnalyticsController {
prevXP = null;
}
Future<void> onAnalyticsUpdate(AnalyticsUpdateType type) async {
if (type == AnalyticsUpdateType.server) {
Future<void> onAnalyticsUpdate(AnalyticsUpdate analyticsUpdate) async {
if (analyticsUpdate.isLogout) return;
if (analyticsUpdate.type == AnalyticsUpdateType.server) {
await getConstructs(forceUpdate: true);
}
updateAnalyticsStream();

View file

@ -21,8 +21,8 @@ enum AnalyticsUpdateType { server, local }
/// 2) constructs used by the user, both in sending messages and doing practice activities
class MyAnalyticsController extends BaseController<AnalyticsStream> {
late PangeaController _pangeaController;
CachedStreamController<AnalyticsUpdateType> analyticsUpdateStream =
CachedStreamController<AnalyticsUpdateType>();
CachedStreamController<AnalyticsUpdate> analyticsUpdateStream =
CachedStreamController<AnalyticsUpdate>();
StreamSubscription<AnalyticsStream>? _analyticsStream;
Timer? _updateTimer;
@ -237,7 +237,9 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
final int newLevel = _pangeaController.analytics.level;
newLevel > prevLevel
? sendLocalAnalyticsToAnalyticsRoom()
: analyticsUpdateStream.add(AnalyticsUpdateType.local);
: analyticsUpdateStream.add(
AnalyticsUpdate(AnalyticsUpdateType.local),
);
}
/// Clears the local cache of recently sent constructs. Called before updating analytics
@ -281,7 +283,9 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
/// for the completion of the previous update and returns. Otherwise, it creates a new [_updateCompleter] and
/// proceeds with the update process. If the update is successful, it clears any messages that were received
/// since the last update and notifies the [analyticsUpdateStream].
Future<void> sendLocalAnalyticsToAnalyticsRoom() async {
Future<void> sendLocalAnalyticsToAnalyticsRoom({
onLogout = false,
}) async {
if (_pangeaController.matrixState.client.userID == null) return;
if (!(_updateCompleter?.isCompleted ?? true)) {
await _updateCompleter!.future;
@ -293,7 +297,12 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
clearMessagesSinceUpdate();
lastUpdated = DateTime.now();
analyticsUpdateStream.add(AnalyticsUpdateType.server);
analyticsUpdateStream.add(
AnalyticsUpdate(
AnalyticsUpdateType.server,
isLogout: onLogout,
),
);
} catch (err, s) {
ErrorHandler.logError(
e: err,
@ -340,3 +349,10 @@ class AnalyticsStream {
required this.constructs,
});
}
class AnalyticsUpdate {
final AnalyticsUpdateType type;
final bool isLogout;
AnalyticsUpdate(this.type, {this.isLogout = false});
}

View file

@ -298,7 +298,8 @@ class PangeaController {
await space.invite(BotName.byEnvironment);
} catch (err) {
ErrorHandler.logError(
e: "Failed to invite pangea bot to space ${space.id}",
e: "Failed to invite pangea bot to existing space",
data: {"spaceId": space.id, "error": err},
);
}
}

View file

@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:collection';
import 'dart:developer';
import 'package:fluffychat/pangea/constants/local.key.dart';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
@ -26,66 +25,60 @@ class PracticeActivityRecordController {
static const int maxStoredEvents = 100;
static final Map<int, _RecordCacheItem> _cache = {};
late final PangeaController _pangeaController;
Timer? _cacheClearTimer;
PracticeActivityRecordController(this._pangeaController) {
_initializeCacheClearing();
PracticeActivityRecordController(this._pangeaController);
int getCompletedActivityCount(String messageID) {
return _completedActivities[messageID] ?? 0;
}
LinkedHashMap<String, int> get completedActivities {
try {
final dynamic locallySaved = _pangeaController.pStoreService.read(
PLocalKey.completedActivities,
);
if (locallySaved == null) return LinkedHashMap<String, int>();
try {
final LinkedHashMap<String, int> cache =
LinkedHashMap<String, int>.from(locallySaved);
return cache;
} catch (err) {
_pangeaController.pStoreService.delete(
PLocalKey.completedActivities,
);
return LinkedHashMap<String, int>();
}
} catch (exception, stackTrace) {
ErrorHandler.logError(
e: PangeaWarningError(
"Failed to get completed activities from cache: $exception",
),
s: stackTrace,
m: 'Failed to get completed activities from cache',
);
return LinkedHashMap<String, int>();
}
}
final LinkedHashMap<String, int> _completedActivities =
LinkedHashMap<String, int>();
// LinkedHashMap<String, int> get _completedActivities {
// try {
// final dynamic locallySaved = _pangeaController.pStoreService.read(
// PLocalKey.completedActivities,
// );
// if (locallySaved == null) return LinkedHashMap<String, int>();
// try {
// final LinkedHashMap<String, int> cache =
// LinkedHashMap<String, int>.from(locallySaved);
// return cache;
// } catch (err) {
// _pangeaController.pStoreService.delete(
// PLocalKey.completedActivities,
// );
// return LinkedHashMap<String, int>();
// }
// } catch (exception, stackTrace) {
// ErrorHandler.logError(
// e: PangeaWarningError(
// "Failed to get completed activities from cache: $exception",
// ),
// s: stackTrace,
// m: 'Failed to get completed activities from cache',
// );
// return LinkedHashMap<String, int>();
// }
// }
Future<void> completeActivity(String messageID) async {
final LinkedHashMap<String, int> currentCache = completedActivities;
final numCompleted = currentCache[messageID] ?? 0;
currentCache[messageID] = numCompleted + 1;
final numCompleted = _completedActivities[messageID] ?? 0;
_completedActivities[messageID] = numCompleted + 1;
// final LinkedHashMap<String, int> currentCache = _completedActivities;
// final numCompleted = currentCache[messageID] ?? 0;
// currentCache[messageID] = numCompleted + 1;
if (currentCache.length > maxStoredEvents) {
currentCache.remove(currentCache.keys.first);
}
// if (currentCache.length > maxStoredEvents) {
// currentCache.remove(currentCache.keys.first);
// }
await _pangeaController.pStoreService.save(
PLocalKey.completedActivities,
currentCache,
);
}
void _initializeCacheClearing() {
const duration = Duration(minutes: 2);
_cacheClearTimer = Timer.periodic(duration, (Timer t) => _clearCache());
}
void _clearCache() {
_cache.clear();
}
void dispose() {
_cacheClearTimer?.cancel();
// await _pangeaController.pStoreService.save(
// PLocalKey.completedActivities,
// currentCache,
// );
debugPrint("completed activities is now: $_completedActivities");
}
/// Sends a practice activity record to the server and returns the corresponding event.

View file

@ -23,7 +23,7 @@ import 'package:http/http.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
import 'package:url_launcher/url_launcher_string.dart';
enum CanSendStatus {
enum SubscriptionStatus {
subscribed,
dimissedPaywall,
showPaywall,
@ -227,11 +227,13 @@ class SubscriptionController extends BaseController {
setState(null);
}
CanSendStatus get canSendStatus => isSubscribed
? CanSendStatus.subscribed
/// if the user is subscribed, returns subscribed
/// if the user has dismissed the paywall, returns dismissed
SubscriptionStatus get subscriptionStatus => isSubscribed
? SubscriptionStatus.subscribed
: _shouldShowPaywall
? CanSendStatus.showPaywall
: CanSendStatus.dimissedPaywall;
? SubscriptionStatus.showPaywall
: SubscriptionStatus.dimissedPaywall;
DateTime? get _lastDismissedPaywall {
final lastDismissed = _pangeaController.pStoreService.read(
@ -249,6 +251,7 @@ class SubscriptionController extends BaseController {
return backoff;
}
/// whether or not the paywall should be shown
bool get _shouldShowPaywall {
return initialized.isCompleted &&
!isSubscribed &&

View file

@ -80,17 +80,23 @@ class TTSToken {
class TextToSpeechRequest {
String text;
String langCode;
String userL1;
String userL2;
List<PangeaTokenText> tokens;
TextToSpeechRequest({
required this.text,
required this.langCode,
required this.userL1,
required this.userL2,
required this.tokens,
});
Map<String, dynamic> toJson() => {
ModelKey.text: text,
ModelKey.langCode: langCode,
ModelKey.userL1: userL1,
ModelKey.userL2: userL2,
ModelKey.tokens: tokens.map((token) => token.toJson()).toList(),
};

View file

@ -121,19 +121,26 @@ class UserController extends BaseController {
/// Initializes the user's profile by waiting for account data to load, reading in account
/// data to profile, and migrating from the pangea profile if the account data is not present.
Future<void> _initialize() async {
// wait for account data to load
// as long as it's not null, then this we've already migrated the profile
await _pangeaController.matrixState.client.waitForAccountData();
if (profile.userSettings.dateOfBirth != null) {
return;
}
// we used to store the user's profile in the pangea server
// we now store it in the matrix account data
final PangeaProfileResponse? resp = await PUserRepo.fetchPangeaUserInfo(
userID: userId!,
matrixAccessToken: _matrixAccessToken!,
);
// if it's null, we don't have a profile in the pangea server
if (resp?.profile == null) {
return;
}
// if we have a profile in the pangea server, we need to migrate it to the matrix account data
final userSetting = UserSettings.fromJson(resp!.profile.toJson());
final newProfile = Profile(userSettings: userSetting);
await newProfile.saveProfileData(waitForDataInSync: true);

View file

@ -125,3 +125,12 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
}
}
}
class ConstructUseTypeUtil {
static ConstructUseTypeEnum fromString(String value) {
return ConstructUseTypeEnum.values.firstWhere(
(e) => e.string == value,
orElse: () => ConstructUseTypeEnum.nan,
);
}
}

View file

@ -93,6 +93,8 @@ class PangeaMessageEvent {
text: rep.content.text,
tokens: (await rep.tokensGlobal(context)).map((t) => t.text).toList(),
langCode: langCode,
userL1: l1Code ?? LanguageKeys.unknownLanguage,
userL2: l2Code ?? LanguageKeys.unknownLanguage,
);
final TextToSpeechResponse response =
@ -538,8 +540,7 @@ class PangeaMessageEvent {
int get numberOfActivitiesCompleted {
return MatrixState.pangeaController.activityRecordController
.completedActivities[eventId] ??
0;
.getCompletedActivityCount(eventId);
}
String? get l2Code =>

View file

@ -36,13 +36,8 @@ class PracticeActivityEvent {
}
PracticeActivityModel get practiceActivity {
try {
_content ??= event.getPangeaContent<PracticeActivityModel>();
return _content!;
} catch (e, s) {
final contentMap = event.content;
rethrow;
}
_content ??= event.getPangeaContent<PracticeActivityModel>();
return _content!;
}
/// All completion records assosiated with this activity

View file

@ -1,6 +1,5 @@
import 'dart:developer';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
@ -106,9 +105,7 @@ class OneConstructUse {
debugger(when: kDebugMode && constructType == null);
return OneConstructUse(
useType: ConstructUseTypeEnum.values
.firstWhereOrNull((e) => e.string == json['useType']) ??
ConstructUseTypeEnum.unk,
useType: ConstructUseTypeUtil.fromString(json['useType']),
lemma: json['lemma'],
form: json['form'],
categories: json['categories'] != null

View file

@ -27,12 +27,7 @@ class ConstructWithXP {
? DateTime.parse(json['last_used'] as String)
: null,
condensedConstructUses: (json['uses'] as List<String>).map((e) {
return ConstructUseTypeEnum.values.firstWhereOrNull(
(element) =>
element.string == e ||
element.toString().split('.').last == e,
) ??
ConstructUseTypeEnum.nan;
return ConstructUseTypeUtil.fromString(e);
}).toList(),
);
}

View file

@ -187,10 +187,10 @@ class PracticeActivityModel {
// moving from multiple_choice to content as the key
// this is to make the model more generic
// here for backward compatibility
final Map<String, dynamic>? content =
final Map<String, dynamic>? contentMap =
(json['content'] ?? json["multiple_choice"]) as Map<String, dynamic>?;
if (content == null) {
if (contentMap == null) {
Sentry.addBreadcrumb(
Breadcrumb(data: {"json": json}),
);
@ -211,9 +211,7 @@ class PracticeActivityModel {
e.string == json['activity_type'] as String ||
e.string.split('.').last == json['activity_type'] as String,
),
content: ActivityContent.fromJson(
content,
),
content: ActivityContent.fromJson(contentMap),
);
}

View file

@ -8,11 +8,7 @@ import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
class SettingsLearning extends StatefulWidget {
final bool isPopup;
const SettingsLearning({
this.isPopup = false,
super.key,
});
const SettingsLearning({super.key});
@override
SettingsLearningController createState() => SettingsLearningController();

View file

@ -4,6 +4,7 @@ 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/p_settings_switch_list_tile.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -13,18 +14,16 @@ class SettingsLearningView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
final dialogContent = Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(
L10n.of(context)!.learningSettings,
),
leading: controller.widget.isPopup
? IconButton(
icon: const Icon(Icons.close),
onPressed: Navigator.of(context).pop,
)
: null,
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: Navigator.of(context).pop,
),
),
body: ListTileTheme(
iconColor: Theme.of(context).textTheme.bodyLarge!.color,
@ -79,5 +78,25 @@ class SettingsLearningView extends StatelessWidget {
),
),
);
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,
),
);
}
}

View file

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
@ -27,13 +26,6 @@ void findConversationPartnerDialog(
onPressed: Navigator.of(context).pop,
child: Text(L10n.of(context)!.cancel),
),
TextButton(
onPressed: () {
context.go('/rooms/settings/learning');
Navigator.of(context).pop();
},
child: Text(L10n.of(context)!.accountSettings),
),
],
),
);

View file

@ -30,6 +30,7 @@ class InlineTooltip extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
// Lightbulb icon on the left
Icon(
@ -39,16 +40,14 @@ class InlineTooltip extends StatelessWidget {
),
const SizedBox(width: 8),
// Text in the middle
Expanded(
child: Center(
child: Text(
instructionsEnum.body(context),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
height: 1.5,
),
textAlign: TextAlign.left,
Center(
child: Text(
instructionsEnum.body(context),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
height: 1.5,
),
textAlign: TextAlign.left,
),
),
// Close button on the right

View file

@ -21,7 +21,7 @@ void pLogoutAction(BuildContext context, {bool? isDestructiveAction}) async {
// before wiping out locally cached construct data, save it to the server
await MatrixState.pangeaController.myAnalytics
.sendLocalAnalyticsToAnalyticsRoom();
.sendLocalAnalyticsToAnalyticsRoom(onLogout: true);
await showFutureLoadingDialog(
context: context,

View file

@ -20,17 +20,14 @@ class OverlayUtil {
required BuildContext context,
required Widget child,
required String transformTargetId,
double? width,
double? height,
backDropToDismiss = true,
blurBackground = false,
Color? borderColor,
Color? backgroundColor,
Alignment? targetAnchor,
Alignment? followerAnchor,
bool closePrevOverlay = true,
Function? onDismiss,
OverlayPositionEnum position = OverlayPositionEnum.transform,
Offset? offset,
}) {
try {
if (closePrevOverlay) {
@ -54,18 +51,16 @@ class OverlayUtil {
right: (position == OverlayPositionEnum.centered) ? 0 : null,
left: (position == OverlayPositionEnum.centered) ? 0 : null,
bottom: (position == OverlayPositionEnum.centered) ? 0 : null,
width: width,
height: height,
child: (position != OverlayPositionEnum.transform)
? child
: CompositedTransformFollower(
targetAnchor: targetAnchor ?? Alignment.topCenter,
followerAnchor:
followerAnchor ?? Alignment.bottomCenter,
targetAnchor: Alignment.topCenter,
followerAnchor: Alignment.bottomCenter,
link: MatrixState.pAnyState
.layerLinkAndKey(transformTargetId)
.link,
showWhenUnlinked: false,
offset: offset ?? Offset.zero,
child: child,
),
),
@ -100,6 +95,32 @@ class OverlayUtil {
return;
}
Offset offset = Offset.zero;
final RenderBox? targetRenderBox =
layerLinkAndKey.key.currentContext!.findRenderObject() as RenderBox?;
if (targetRenderBox != null && targetRenderBox.hasSize) {
final Offset transformTargetOffset =
(targetRenderBox).localToGlobal(Offset.zero);
final Size transformTargetSize = targetRenderBox.size;
final horizontalMidpoint =
transformTargetOffset.dx + (transformTargetSize.width / 2);
final halfMaxWidth = maxWidth / 2;
final hasLeftOverflow = (horizontalMidpoint - halfMaxWidth) < 0;
final hasRightOverflow = (horizontalMidpoint + halfMaxWidth) >
MediaQuery.of(context).size.width;
double xOffset = 0;
MediaQuery.of(context).size.width - (horizontalMidpoint + halfMaxWidth);
if (hasLeftOverflow) {
xOffset = (transformTargetOffset.dx - halfMaxWidth) * -1;
} else if (hasRightOverflow) {
xOffset = MediaQuery.of(context).size.width -
(horizontalMidpoint + halfMaxWidth);
}
offset = Offset(xOffset, 0);
}
final Widget child = Material(
borderOnForeground: false,
color: Colors.transparent,
@ -119,6 +140,7 @@ class OverlayUtil {
backDropToDismiss: backDropToDismiss,
borderColor: borderColor,
closePrevOverlay: closePrevOverlay,
offset: offset,
);
} catch (err, stack) {
debugger(when: kDebugMode);
@ -138,12 +160,12 @@ class OverlayUtil {
// final OverlayConstraints constraints =
// ChatViewConstraints(transformTargetContext);
// final RenderObject? targetRenderBox =
// transformTargetContext.findRenderObject();
// if (targetRenderBox == null) return Offset.zero;
// final Offset transformTargetOffset =
// (targetRenderBox as RenderBox).localToGlobal(Offset.zero);
// final Size transformTargetSize = targetRenderBox.size;
// final RenderObject? targetRenderBox =
// transformTargetContext.findRenderObject();
// if (targetRenderBox == null) return Offset.zero;
// final Offset transformTargetOffset =
// (targetRenderBox as RenderBox).localToGlobal(Offset.zero);
// final Size transformTargetSize = targetRenderBox.size;
// // ideally horizontally centered on target
// double dx = transformTargetSize.width / 2 - cardSize.width / 2;

View file

@ -1,6 +1,7 @@
import 'dart:developer';
import 'dart:math';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/events/audio_player.dart';
import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
@ -8,7 +9,6 @@ import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dar
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart';
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart';
@ -21,11 +21,15 @@ class MessageAudioCard extends StatefulWidget {
final PangeaMessageEvent messageEvent;
final MessageOverlayController overlayController;
final PangeaTokenText? selection;
final TtsController tts;
final Function(bool) setIsPlayingAudio;
const MessageAudioCard({
super.key,
required this.messageEvent,
required this.overlayController,
required this.tts,
required this.setIsPlayingAudio,
this.selection,
});
@ -40,8 +44,6 @@ class MessageAudioCardState extends State<MessageAudioCard> {
int? sectionStartMS;
int? sectionEndMS;
TtsController tts = TtsController();
@override
void initState() {
super.initState();
@ -56,7 +58,7 @@ class MessageAudioCardState extends State<MessageAudioCard> {
@override
void didUpdateWidget(covariant oldWidget) {
if (oldWidget.selection != widget.selection) {
if (oldWidget.selection != widget.selection && widget.selection != null) {
debugPrint('selection changed');
setSectionStartAndEndFromSelection();
playSelectionAudio();
@ -65,10 +67,11 @@ class MessageAudioCardState extends State<MessageAudioCard> {
}
Future<void> playSelectionAudio() async {
if (widget.selection == null) return;
final PangeaTokenText selection = widget.selection!;
final tokenText = selection.content;
await tts.speak(tokenText);
await widget.tts.speak(tokenText);
}
void setSectionStartAndEnd(int? start, int? end) => mounted
@ -89,8 +92,7 @@ class MessageAudioCardState extends State<MessageAudioCard> {
// should never happen but just in case
debugger(when: kDebugMode);
ErrorHandler.logError(
e: Exception(),
m: 'audioFile duration is null in MessageAudioCardState',
e: 'audioFile duration is null in MessageAudioCardState',
data: {
'audioFile': audioFile,
},
@ -124,8 +126,7 @@ class MessageAudioCardState extends State<MessageAudioCard> {
// if we didn't find the token, we should pause if debug and log an error
debugger(when: kDebugMode);
ErrorHandler.logError(
e: Exception(),
m: 'could not find token for selection in MessageAudioCardState',
e: 'could not find token for selection in MessageAudioCardState',
data: {
'selection': selection,
'tokens': tokens,
@ -174,7 +175,7 @@ class MessageAudioCardState extends State<MessageAudioCard> {
),
);
ErrorHandler.logError(
e: Exception(),
e: e,
s: s,
m: 'something wrong getting audio in MessageAudioCardState',
data: {
@ -192,7 +193,6 @@ class MessageAudioCardState extends State<MessageAudioCard> {
children: [
Container(
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(minHeight: minCardHeight),
alignment: Alignment.center,
child: _isLoading
? const ToolbarContentLoadingIndicator()
@ -206,11 +206,15 @@ class MessageAudioCardState extends State<MessageAudioCard> {
sectionEndMS: sectionEndMS,
color:
Theme.of(context).colorScheme.onPrimaryContainer,
setIsPlayingAudio: widget.setIsPlayingAudio,
),
tts.missingVoiceButton,
widget.tts.missingVoiceButton,
],
)
: const CardErrorWidget(),
: const CardErrorWidget(
error: "Null audio file in message_audio_card",
maxWidth: AppConfig.toolbarMinWidth,
),
),
],
);

View file

@ -11,11 +11,13 @@ import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar_buttons.dart';
import 'package:fluffychat/pangea/widgets/chat/overlay_footer.dart';
import 'package:fluffychat/pangea/widgets/chat/overlay_header.dart';
import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart';
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
@ -60,7 +62,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
/// The number of activities that need to be completed before the toolbar is unlocked
/// If we don't have any good activities for them, we'll decrease this number
static const int neededActivities = 3;
int activitiesLeftToComplete = neededActivities;
bool get messageInUserL2 =>
@ -69,6 +70,9 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
PangeaMessageEvent get pangeaMessageEvent => widget._pangeaMessageEvent;
final TtsController tts = TtsController();
bool isPlayingAudio = false;
@override
void initState() {
super.initState();
@ -101,6 +105,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
).listen((_) => setState(() {}));
setInitialToolbarMode();
tts.setupTTS();
}
/// We need to check if the setState call is safe to call immediately
@ -108,16 +113,29 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
/// This is a workaround to prevent that error
@override
void setState(VoidCallback fn) {
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle ||
SchedulerBinding.instance.schedulerPhase ==
SchedulerPhase.postFrameCallbacks) {
final phase = SchedulerBinding.instance.schedulerPhase;
if (mounted &&
(phase == SchedulerPhase.idle ||
phase == SchedulerPhase.postFrameCallbacks)) {
// It's safe to call setState immediately
super.setState(fn);
try {
super.setState(fn);
} catch (e, s) {
ErrorHandler.logError(
e: "Error calling setState in MessageSelectionOverlay: $e",
s: s,
);
}
} else {
// Defer the setState call to after the current frame
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
super.setState(fn);
try {
if (mounted) super.setState(fn);
} catch (e, s) {
ErrorHandler.logError(
e: "Error calling setState in MessageSelectionOverlay after postframeCallback: $e",
s: s,
);
}
});
}
@ -193,9 +211,10 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
PangeaToken token,
) {
if ([
MessageMode.practiceActivity,
// MessageMode.textToSpeech
].contains(toolbarMode)) {
MessageMode.practiceActivity,
// MessageMode.textToSpeech
].contains(toolbarMode) ||
isPlayingAudio) {
return;
}
@ -266,17 +285,23 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
double get reactionsHeight => hasReactions ? 28 : 0;
double get belowMessageHeight => toolbarButtonsHeight + reactionsHeight;
void setIsPlayingAudio(bool isPlaying) {
if (mounted) {
setState(() => isPlayingAudio = isPlaying);
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (messageSize == null || messageOffset == null) {
if (messageSize == null || messageOffset == null || screenHeight == null) {
return;
}
// position the overlay directly over the underlying message
final headerBottomOffset = screenHeight - headerHeight;
final headerBottomOffset = screenHeight! - headerHeight;
final footerBottomOffset = footerHeight;
final currentBottomOffset = screenHeight -
final currentBottomOffset = screenHeight! -
messageOffset!.dy -
messageSize!.height -
belowMessageHeight;
@ -304,7 +329,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
animationEndOffset = midpoint - messageSize!.height - belowMessageHeight;
final totalTopOffset =
animationEndOffset + messageSize!.height + AppConfig.toolbarMaxHeight;
final remainingSpace = screenHeight - totalTopOffset;
final remainingSpace = screenHeight! - totalTopOffset;
if (remainingSpace < headerHeight) {
// the overlay could run over the header, so it needs to be shifted down
animationEndOffset -= (headerHeight - remainingSpace);
@ -319,7 +344,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
// update the message height to fit the screen. The message is scrollable, so
// this will make the both the toolbar box and the toolbar buttons visible.
if (animationEndOffset < footerHeight + belowMessageHeight) {
final double remainingSpace = screenHeight -
final double remainingSpace = screenHeight! -
AppConfig.toolbarMaxHeight -
headerHeight -
footerHeight -
@ -354,31 +379,66 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
void dispose() {
_animationController.dispose();
_reactionSubscription?.cancel();
tts.dispose();
super.dispose();
}
RenderBox? get messageRenderBox => MatrixState.pAnyState.getRenderBox(
RenderBox? get messageRenderBox {
try {
return MatrixState.pAnyState.getRenderBox(
widget._event.eventId,
);
} catch (e, s) {
ErrorHandler.logError(e: "Error getting message render box: $e", s: s);
return null;
}
}
Size? get messageSize {
try {
return messageRenderBox?.size;
} catch (e, s) {
ErrorHandler.logError(e: "Error getting message size: $e", s: s);
return null;
}
}
Offset? get messageOffset {
try {
return messageRenderBox?.localToGlobal(Offset.zero);
} catch (e, s) {
ErrorHandler.logError(e: "Error getting message offset: $e", s: s);
return null;
}
}
Size? get messageSize => messageRenderBox?.size;
Offset? get messageOffset => messageRenderBox?.localToGlobal(Offset.zero);
double? adjustedMessageHeight;
// height of the reply/forward bar + the reaction picker + contextual padding
double get footerHeight =>
48 + 56 + (FluffyThemes.isColumnMode(context) ? 16.0 : 8.0);
MediaQueryData? get mediaQuery {
try {
return MediaQuery.of(context);
} catch (e, s) {
ErrorHandler.logError(e: "Error getting media query: $e", s: s);
return null;
}
}
double get headerHeight =>
(Theme.of(context).appBarTheme.toolbarHeight ?? 56) +
MediaQuery.of(context).padding.top;
(mediaQuery?.padding.top ?? 0);
double get screenHeight => MediaQuery.of(context).size.height;
double? get screenHeight => mediaQuery?.size.height;
double get screenWidth => MediaQuery.of(context).size.width;
double? get screenWidth => mediaQuery?.size.width;
@override
Widget build(BuildContext context) {
if (messageSize == null) return const SizedBox.shrink();
final bool showDetails = (Matrix.of(context)
.store
.getBool(SettingKeys.displayChatDetailsColumn) ??
@ -389,20 +449,22 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
// the default spacing between the side of the screen and the message bubble
const double messageMargin = Avatar.defaultSize + 16 + 8;
final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0;
final chatViewWidth = screenWidth -
(FluffyThemes.isColumnMode(context)
? (FluffyThemes.columnWidth + FluffyThemes.navRailWidth)
: 0);
const totalMaxWidth = (FluffyThemes.columnWidth * 2.5) - messageMargin;
double maxWidth = chatViewWidth - (2 * horizontalPadding) - messageMargin;
if (maxWidth > totalMaxWidth) {
double? maxWidth;
if (screenWidth != null) {
final chatViewWidth = screenWidth! -
(FluffyThemes.isColumnMode(context)
? (FluffyThemes.columnWidth + FluffyThemes.navRailWidth)
: 0);
maxWidth = chatViewWidth - (2 * horizontalPadding) - messageMargin;
}
if (maxWidth == null || maxWidth > totalMaxWidth) {
maxWidth = totalMaxWidth;
}
final overlayMessage = Container(
constraints: BoxConstraints(
maxWidth: maxWidth,
),
constraints: BoxConstraints(maxWidth: maxWidth),
child: Material(
type: MaterialType.transparency,
child: Column(
@ -414,6 +476,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
MessageToolbar(
pangeaMessageEvent: widget._pangeaMessageEvent,
overLayController: this,
tts: tts,
),
SizedBox(
height: adjustedMessageHeight,
@ -454,27 +517,33 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
? FluffyThemes.columnWidth + FluffyThemes.navRailWidth
: 0;
final double? leftPadding = widget._pangeaMessageEvent.ownMessage
? null
: messageOffset!.dx - horizontalPadding - columnOffset;
final double? leftPadding =
(widget._pangeaMessageEvent.ownMessage || messageOffset == null)
? null
: messageOffset!.dx - horizontalPadding - columnOffset;
final double? rightPadding = widget._pangeaMessageEvent.ownMessage
? screenWidth -
final double? rightPadding = (widget._pangeaMessageEvent.ownMessage &&
screenWidth != null &&
messageOffset != null &&
messageSize != null)
? screenWidth! -
messageOffset!.dx -
messageSize!.width -
horizontalPadding
: null;
final positionedOverlayMessage = _overlayPositionAnimation == null
? Positioned(
left: leftPadding,
right: rightPadding,
bottom: screenHeight -
messageOffset!.dy -
messageSize!.height -
belowMessageHeight,
child: overlayMessage,
)
final positionedOverlayMessage = (_overlayPositionAnimation == null)
? (screenHeight == null || messageSize == null || messageOffset == null)
? const SizedBox.shrink()
: Positioned(
left: leftPadding,
right: rightPadding,
bottom: screenHeight! -
messageOffset!.dy -
messageSize!.height -
belowMessageHeight,
child: overlayMessage,
)
: AnimatedBuilder(
animation: _overlayPositionAnimation!,
builder: (context, child) {

View file

@ -1,5 +1,6 @@
import 'dart:developer';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/speech_to_text_models.dart';
@ -148,9 +149,12 @@ class MessageSpeechToTextCardState extends State<MessageSpeechToTextCard> {
return const ToolbarContentLoadingIndicator();
}
//done fetchig but not results means some kind of error
// done fetchig but not results means some kind of error
if (speechToTextResponse == null) {
return CardErrorWidget(error: error);
return CardErrorWidget(
error: error,
maxWidth: AppConfig.toolbarMinWidth,
);
}
//TODO: find better icons

View file

@ -11,6 +11,7 @@ import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
import 'package:fluffychat/pangea/widgets/chat/message_speech_to_text_card.dart';
import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart';
import 'package:fluffychat/pangea/widgets/chat/message_unsubscribed_card.dart';
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
import 'package:fluffychat/pangea/widgets/select_to_define.dart';
@ -23,11 +24,13 @@ const double minCardHeight = 70;
class MessageToolbar extends StatelessWidget {
final PangeaMessageEvent pangeaMessageEvent;
final MessageOverlayController overLayController;
final TtsController tts;
const MessageToolbar({
super.key,
required this.pangeaMessageEvent,
required this.overLayController,
required this.tts,
});
Widget get toolbarContent {
@ -66,6 +69,8 @@ class MessageToolbar extends StatelessWidget {
messageEvent: pangeaMessageEvent,
overlayController: overLayController,
selection: overLayController.selectedSpan,
tts: tts,
setIsPlayingAudio: overLayController.setIsPlayingAudio,
);
case MessageMode.speechToText:
return MessageSpeechToTextCard(
@ -103,6 +108,7 @@ class MessageToolbar extends StatelessWidget {
return PracticeActivityCard(
pangeaMessageEvent: pangeaMessageEvent,
overlayController: overLayController,
tts: tts,
);
default:
debugger(when: kDebugMode);
@ -130,6 +136,9 @@ class MessageToolbar extends StatelessWidget {
),
constraints: const BoxConstraints(
maxHeight: AppConfig.toolbarMaxHeight,
minWidth: AppConfig.toolbarMinWidth,
minHeight: AppConfig.toolbarMinHeight,
// maxWidth is set by MessageSelectionOverlay
),
child: SingleChildScrollView(
child: AnimatedSize(

View file

@ -1,3 +1,4 @@
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
@ -130,42 +131,51 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
if (!_fetchingTranslation &&
repEvent == null &&
selectionTranslation == null) {
return const CardErrorWidget();
return const CardErrorWidget(
error: "No translation found",
maxWidth: AppConfig.toolbarMinWidth,
);
}
final loadingTranslation =
(widget.selection != null && selectionTranslation == null) ||
(widget.selection == null && repEvent == null);
if (_fetchingTranslation || loadingTranslation) {
return const ToolbarContentLoadingIndicator();
}
return Padding(
padding: const EdgeInsets.all(8),
padding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
_fetchingTranslation
? const ToolbarContentLoadingIndicator()
: Flexible(
child: Column(
children: [
widget.selection != null
? Text(
selectionTranslation!,
style: BotStyle.text(context),
)
: Text(
repEvent!.text,
style: BotStyle.text(context),
),
if (notGoingToTranslate && widget.selection == null)
InlineTooltip(
instructionsEnum: InstructionsEnum.l1Translation,
onClose: () => setState(() {}),
),
if (widget.selection != null)
InlineTooltip(
instructionsEnum:
InstructionsEnum.clickAgainToDeselect,
onClose: () => setState(() {}),
),
],
),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
widget.selection != null
? selectionTranslation!
: repEvent!.text,
style: BotStyle.text(context),
textAlign: TextAlign.center,
),
if (notGoingToTranslate && widget.selection == null)
InlineTooltip(
instructionsEnum: InstructionsEnum.l1Translation,
onClose: () => setState(() {}),
),
if (widget.selection != null)
InlineTooltip(
instructionsEnum: InstructionsEnum.clickAgainToDeselect,
onClose: () => setState(() {}),
),
],
),
),
],
),
);

View file

@ -18,8 +18,8 @@ class MessageUnsubscribedCard extends StatelessWidget {
final bool inTrialWindow =
MatrixState.pangeaController.userController.inTrialWindow;
return Container(
padding: const EdgeInsets.all(8),
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Text(

View file

@ -49,6 +49,8 @@ class MissingVoiceButton extends StatelessWidget {
),
TextButton(
onPressed: () => launchTTSSettings,
// commenting out as suspecting this is causing an issue
// #freeze-activity
style: const ButtonStyle(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),

View file

@ -1,4 +1,4 @@
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:flutter/material.dart';
class ToolbarContentLoadingIndicator extends StatelessWidget {
@ -8,10 +8,9 @@ class ToolbarContentLoadingIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(minHeight: minCardHeight),
alignment: Alignment.center,
return SizedBox(
width: AppConfig.toolbarMinWidth,
height: AppConfig.toolbarMinHeight,
child: Center(
child: SizedBox(
height: 14,

View file

@ -23,7 +23,8 @@ class TtsController {
}
onError(dynamic message) => ErrorHandler.logError(
m: 'TTS error',
e: message,
m: (message.toString().isNotEmpty) ? message.toString() : 'TTS error',
data: {
'message': message,
},
@ -82,13 +83,11 @@ class TtsController {
debugger(when: kDebugMode);
ErrorHandler.logError(e: e, s: s);
}
await tts.stop();
}
Future<void> speak(String text) async {
try {
stop();
targetLanguage ??=
MatrixState.pangeaController.languageController.userL2?.langCode;
@ -96,7 +95,7 @@ class TtsController {
// return type is dynamic but apparent its supposed to be 1
// https://pub.dev/packages/flutter_tts
if (result != 1) {
if (result != 1 && !kIsWeb) {
ErrorHandler.logError(
m: 'Unexpected result from tts.speak',
data: {

View file

@ -32,6 +32,8 @@ class OverlayContainer extends StatelessWidget {
constraints: BoxConstraints(
maxWidth: maxWidth,
maxHeight: maxHeight,
minHeight: 100,
minWidth: 100,
),
//PTODO - position card above input/message
// margin: const EdgeInsets.all(10),

View file

@ -27,6 +27,8 @@ class ConversationBotModeDynamicZone extends StatelessWidget {
decoration: InputDecoration(
hintText: L10n.of(context)!
.conversationBotDiscussionZone_discussionTopicPlaceholder,
contentPadding:
const EdgeInsets.symmetric(horizontal: 28.0, vertical: 12.0),
),
controller: discussionTopicController,
validator: (value) => enabled &&
@ -44,6 +46,7 @@ class ConversationBotModeDynamicZone extends StatelessWidget {
decoration: InputDecoration(
hintText: L10n.of(context)!
.conversationBotDiscussionZone_discussionKeywordsPlaceholder,
contentPadding: const EdgeInsets.symmetric(horizontal: 28.0),
),
controller: discussionKeywordsController,
enabled: enabled,
@ -58,6 +61,7 @@ class ConversationBotModeDynamicZone extends StatelessWidget {
decoration: InputDecoration(
hintText: L10n.of(context)!
.conversationBotCustomZone_customSystemPromptPlaceholder,
contentPadding: const EdgeInsets.symmetric(horizontal: 28.0),
),
validator: (value) => enabled &&
botOptions.mode == BotMode.custom &&

View file

@ -1,3 +1,4 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:fluffychat/pangea/constants/bot_mode.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -26,23 +27,8 @@ class ConversationBotModeSelect extends StatelessWidget {
// L10n.of(context)!.conversationBotModeSelectOption_storyGame,
};
String? mode = initialMode;
if (!options.containsKey(initialMode)) {
mode = null;
}
return DropdownButtonFormField(
// Initial Value
hint: Text(
options[mode ?? BotMode.discussion]!,
overflow: TextOverflow.clip,
textAlign: TextAlign.center,
),
// ),
isExpanded: true,
// Down Arrow Icon
icon: const Icon(Icons.keyboard_arrow_down),
// Array list of items
return DropdownButtonFormField2(
hint: Text(L10n.of(context)!.selectBotChatMode),
items: [
for (final entry in options.entries)
DropdownMenuItem(

View file

@ -1,3 +1,4 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:fluffychat/pangea/models/bot_options_model.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart';
@ -36,8 +37,10 @@ class ConversationBotSettingsForm extends StatelessWidget {
Widget build(BuildContext context) {
return Column(
children: [
DropdownButtonFormField(
// Initial Value
DropdownButtonFormField2(
dropdownStyleData: const DropdownStyleData(
padding: EdgeInsets.zero,
),
hint: Text(
L10n.of(context)!.selectBotLanguage,
overflow: TextOverflow.clip,
@ -45,7 +48,6 @@ class ConversationBotSettingsForm extends StatelessWidget {
),
value: botOptions.targetLanguage,
isExpanded: true,
icon: const Icon(Icons.keyboard_arrow_down),
items: MatrixState.pangeaController.pLanguageStore.targetOptions
.map((language) {
return DropdownMenuItem(
@ -60,8 +62,7 @@ class ConversationBotSettingsForm extends StatelessWidget {
onChanged: enabled ? onUpdateBotLanguage : null,
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
// Initial Value
DropdownButtonFormField2<String>(
hint: Text(
L10n.of(context)!.chooseVoice,
overflow: TextOverflow.clip,
@ -69,7 +70,6 @@ class ConversationBotSettingsForm extends StatelessWidget {
),
value: botOptions.targetVoice,
isExpanded: true,
icon: const Icon(Icons.keyboard_arrow_down),
items: const [],
onChanged: enabled ? onUpdateBotVoice : null,
),

View file

@ -1,7 +1,6 @@
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/utils/bot_style.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
import 'package:fluffychat/pangea/widgets/common/bot_face_svg.dart';
import 'package:fluffychat/pangea/widgets/igc/card_header.dart';
import 'package:flutter/material.dart';
@ -10,41 +9,44 @@ class CardErrorWidget extends StatelessWidget {
final Object? error;
final Choreographer? choreographer;
final int? offset;
final double? maxWidth;
const CardErrorWidget({
super.key,
this.error,
this.choreographer,
this.offset,
this.maxWidth,
});
@override
Widget build(BuildContext context) {
final ErrorCopy errorCopy = ErrorCopy(context, error);
return Container(
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(minHeight: minCardHeight),
alignment: Alignment.center,
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CardHeader(
text: errorCopy.title,
botExpression: BotExpression.addled,
onClose: () => choreographer?.onMatchError(
cursorOffset: offset,
),
return ConstrainedBox(
constraints: maxWidth != null
? BoxConstraints(maxWidth: maxWidth!)
: const BoxConstraints(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CardHeader(
text: errorCopy.title,
botExpression: BotExpression.addled,
onClose: () => choreographer?.onMatchError(
cursorOffset: offset,
),
const SizedBox(height: 10.0),
Center(
child: Text(
errorCopy.body,
style: BotStyle.text(context),
),
),
const SizedBox(height: 12.0),
Padding(
padding: const EdgeInsets.all(12),
child: Text(
errorCopy.body,
style: BotStyle.text(context),
textAlign: TextAlign.center,
),
],
),
),
],
),
);
}

View file

@ -1,8 +1,8 @@
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/utils/bot_style.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import '../../../widgets/matrix.dart';
import '../../utils/bot_style.dart';
import '../common/bot_face_svg.dart';
class CardHeader extends StatelessWidget {
@ -23,35 +23,35 @@ class CardHeader extends StatelessWidget {
padding: const EdgeInsets.only(bottom: 5.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(top: 3.0),
child: BotFace(
width: 50.0,
expression: botExpression,
Flexible(
child: Row(
children: [
BotFace(
width: 50.0,
expression: botExpression,
),
const SizedBox(width: 12.0),
Flexible(
child: Text(
text,
style: BotStyle.text(context),
softWrap: true,
),
),
],
),
),
const SizedBox(width: 5.0),
Expanded(
child: Text(
text,
style: BotStyle.text(context),
textAlign: TextAlign.left,
),
),
CircleAvatar(
backgroundColor: AppConfig.primaryColor.withOpacity(0.1),
child: IconButton(
icon: const Icon(Icons.close_outlined),
onPressed: () {
if (onClose != null) onClose!();
MatrixState.pAnyState.closeOverlay();
},
color: Theme.of(context).brightness == Brightness.dark
? AppConfig.primaryColorLight
: AppConfig.primaryColor,
),
IconButton(
icon: const Icon(Icons.close_outlined),
onPressed: () {
if (onClose != null) onClose!();
MatrixState.pAnyState.closeOverlay();
},
color: Theme.of(context).brightness == Brightness.dark
? AppConfig.primaryColorLight
: AppConfig.primaryColor,
),
],
),

View file

@ -47,9 +47,11 @@ class PangeaTextController extends TextEditingController {
debugger(when: kDebugMode);
return;
}
final CanSendStatus canSendStatus =
choreographer.pangeaController.subscriptionController.canSendStatus;
if (canSendStatus == CanSendStatus.showPaywall &&
// show the paywall if appropriate
if (choreographer
.pangeaController.subscriptionController.subscriptionStatus ==
SubscriptionStatus.showPaywall &&
!choreographer.isFetching &&
text.isNotEmpty) {
OverlayUtil.showPositionedCard(
@ -63,11 +65,18 @@ class PangeaTextController extends TextEditingController {
);
}
// if there is no igc text data, then don't do anything
if (choreographer.igc.igcTextData == null) return;
// debugPrint(
// "onInputTap matches are ${choreographer.igc.igcTextData?.matches.map((e) => e.match.rule.id).toList().toString()}");
// if user is just trying to get their cursor into the text input field to add soemthing,
// then don't interrupt them
if (selection.baseOffset >= text.length) {
return;
}
final int tokenIndex = choreographer.igc.igcTextData!.tokenIndexByOffset(
selection.baseOffset,
);
@ -147,9 +156,9 @@ class PangeaTextController extends TextEditingController {
// debugPrint("composing after ${value.composing.textAfter(value.text)}");
// }
final CanSendStatus canSendStatus =
choreographer.pangeaController.subscriptionController.canSendStatus;
if (canSendStatus == CanSendStatus.showPaywall &&
final SubscriptionStatus canSendStatus = choreographer
.pangeaController.subscriptionController.subscriptionStatus;
if (canSendStatus == SubscriptionStatus.showPaywall &&
!choreographer.isFetching &&
text.isNotEmpty) {
return TextSpan(

View file

@ -1,5 +1,6 @@
import 'dart:developer';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/controllers/contextual_definition_controller.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
@ -7,8 +8,7 @@ import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:fluffychat/pangea/utils/bot_style.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
import 'package:fluffychat/pangea/widgets/common/p_circular_loader.dart';
import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -166,71 +166,68 @@ class WordDataCardView extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (controller.wordNetError != null) {
return CardErrorWidget(error: controller.wordNetError);
return CardErrorWidget(
error: controller.wordNetError,
maxWidth: AppConfig.toolbarMinWidth,
);
}
if (controller.activeL1 == null || controller.activeL2 == null) {
ErrorHandler.logError(m: "should not be here");
return CardErrorWidget(error: controller.noLanguages);
return CardErrorWidget(
error: controller.noLanguages,
maxWidth: AppConfig.toolbarMinWidth,
);
}
final ScrollController scrollController = ScrollController();
return Container(
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(minHeight: minCardHeight),
alignment: Alignment.center,
child: Scrollbar(
thumbVisibility: true,
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (controller.widget.choiceFeedback != null)
Text(
controller.widget.choiceFeedback!,
style: BotStyle.text(context),
),
const SizedBox(height: 5.0),
if (controller.wordData != null &&
controller.wordNetError == null &&
controller.activeL1 != null &&
controller.activeL2 != null)
WordNetInfo(
wordData: controller.wordData!,
activeL1: controller.activeL1!,
activeL2: controller.activeL2!,
),
if (controller.isLoadingWordNet) const PCircular(),
const SizedBox(height: 5.0),
// if (controller.widget.hasInfo &&
// !controller.isLoadingContextualDefinition &&
// controller.contextualDefinitionRes == null)
// Material(
// type: MaterialType.transparency,
// child: ListTile(
// leading: const BotFace(
// width: 40, expression: BotExpression.surprised),
// title: Text(L10n.of(context)!.askPangeaBot),
// onTap: controller.handleGetDefinitionButtonPress,
// ),
// ),
if (controller.isLoadingContextualDefinition) const PCircular(),
if (controller.contextualDefinitionRes != null)
Text(
controller.contextualDefinitionRes!.text,
style: BotStyle.text(context),
),
if (controller.definitionError != null)
Text(
L10n.of(context)!.sorryNoResults,
style: BotStyle.text(context),
),
],
),
),
return Padding(
padding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
child: Column(
children: [
if (controller.widget.choiceFeedback != null)
Text(
controller.widget.choiceFeedback!,
style: BotStyle.text(context),
),
const SizedBox(height: 5.0),
if (controller.wordData != null &&
controller.wordNetError == null &&
controller.activeL1 != null &&
controller.activeL2 != null)
WordNetInfo(
wordData: controller.wordData!,
activeL1: controller.activeL1!,
activeL2: controller.activeL2!,
),
if (controller.isLoadingWordNet)
const ToolbarContentLoadingIndicator(),
const SizedBox(height: 5.0),
// if (controller.widget.hasInfo &&
// !controller.isLoadingContextualDefinition &&
// controller.contextualDefinitionRes == null)
// Material(
// type: MaterialType.transparency,
// child: ListTile(
// leading: const BotFace(
// width: 40, expression: BotExpression.surprised),
// title: Text(L10n.of(context)!.askPangeaBot),
// onTap: controller.handleGetDefinitionButtonPress,
// ),
// ),
if (controller.isLoadingContextualDefinition)
const ToolbarContentLoadingIndicator(),
if (controller.contextualDefinitionRes != null)
Text(
controller.contextualDefinitionRes!.text,
style: BotStyle.text(context),
textAlign: TextAlign.center,
),
if (controller.definitionError != null)
Text(
L10n.of(context)!.sorryNoResults,
style: BotStyle.text(context),
textAlign: TextAlign.center,
),
],
),
);
}
@ -251,12 +248,14 @@ class WordNetInfo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SensesForLanguage(
wordData: wordData,
languageType: LanguageType.target,
language: activeL2,
),
const SizedBox(height: 10),
SensesForLanguage(
wordData: wordData,
languageType: LanguageType.base,
@ -273,52 +272,6 @@ enum LanguageType {
}
class SensesForLanguage extends StatelessWidget {
const SensesForLanguage({
super.key,
required this.wordData,
required this.languageType,
required this.language,
});
final LanguageModel language;
final LanguageType languageType;
final WordData wordData;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(7, 0, 0, 0),
child: LanguageFlag(
language: language,
),
),
Expanded(
child: PartOfSpeechBlock(
wordData: wordData,
languageType: languageType,
),
),
],
),
);
}
}
class PartOfSpeechBlock extends StatelessWidget {
final WordData wordData;
final LanguageType languageType;
const PartOfSpeechBlock({
super.key,
required this.wordData,
required this.languageType,
});
String get exampleSentence => languageType == LanguageType.target
? wordData.targetExampleSentence
: wordData.baseExampleSentence;
@ -336,70 +289,76 @@ class PartOfSpeechBlock extends StatelessWidget {
return "$word (${wordData.formattedPartOfSpeech(languageType)})";
}
const SensesForLanguage({
super.key,
required this.wordData,
required this.languageType,
required this.language,
});
final LanguageModel language;
final LanguageType languageType;
final WordData wordData;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Align(
alignment: Alignment.centerLeft,
child: Text(
formattedTitle(context),
style: BotStyle.text(context, italics: true, bold: false),
),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.only(left: 14.0, bottom: 10.0),
child: Align(
alignment: Alignment.centerLeft,
child: Column(
children: [
if (definition.isNotEmpty)
RichText(
text: TextSpan(
style: BotStyle.text(
context,
italics: false,
bold: false,
),
children: <TextSpan>[
TextSpan(
text: "${L10n.of(context)!.definition}: ",
style: const TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: definition),
],
),
),
const SizedBox(height: 10),
if (exampleSentence.isNotEmpty)
RichText(
text: TextSpan(
style: BotStyle.text(
context,
italics: false,
bold: false,
),
children: <TextSpan>[
TextSpan(
text: "${L10n.of(context)!.exampleSentence}: ",
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
TextSpan(text: exampleSentence),
],
),
),
],
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LanguageFlag(language: language),
const SizedBox(width: 10),
Flexible(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
formattedTitle(context),
style: BotStyle.text(context, italics: true, bold: false),
),
),
const SizedBox(height: 4),
if (definition.isNotEmpty)
RichText(
text: TextSpan(
style: BotStyle.text(
context,
italics: false,
bold: false,
),
children: <TextSpan>[
TextSpan(
text: "${L10n.of(context)!.definition}: ",
style: const TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: definition),
],
),
),
const SizedBox(height: 4),
if (exampleSentence.isNotEmpty)
RichText(
text: TextSpan(
style: BotStyle.text(
context,
italics: false,
bold: false,
),
children: <TextSpan>[
TextSpan(
text: "${L10n.of(context)!.exampleSentence}: ",
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
TextSpan(text: exampleSentence),
],
),
),
],
),
],
),
),
],
);
}
}

View file

@ -6,6 +6,7 @@ import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/word_audio_button.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -14,13 +15,15 @@ import 'package:flutter/material.dart';
/// The multiple choice activity view
class MultipleChoiceActivity extends StatefulWidget {
final MessagePracticeActivityCardState practiceCardController;
final PracticeActivityCardState practiceCardController;
final PracticeActivityModel currentActivity;
final TtsController tts;
const MultipleChoiceActivity({
super.key,
required this.practiceCardController,
required this.currentActivity,
required this.tts,
});
@override
@ -67,6 +70,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
return;
}
// #freeze-activity
MatrixState.pangeaController.myAnalytics.setState(
AnalyticsStream(
// note - this maybe should be the activity event id
@ -85,16 +89,18 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
widget.practiceCardController.onActivityFinish();
}
setState(
() => selectedChoiceIndex = index,
);
if (mounted) {
setState(
() => selectedChoiceIndex = index,
);
}
}
@override
Widget build(BuildContext context) {
final PracticeActivityModel practiceActivity = widget.currentActivity;
return Container(
return Padding(
padding: const EdgeInsets.all(8),
child: Column(
children: [
@ -106,9 +112,13 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
),
),
const SizedBox(height: 8),
// #freeze-activity
if (practiceActivity.activityType ==
ActivityTypeEnum.wordFocusListening)
WordAudioButton(text: practiceActivity.content.answer),
WordAudioButton(
text: practiceActivity.content.answer,
ttsController: widget.tts,
),
ChoicesArray(
isLoading: false,
uniqueKeyForLayerLink: (index) => "multiple_choice_$index",

View file

@ -1,3 +1,4 @@
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/utils/bot_style.dart';
import 'package:flutter/material.dart';
@ -71,26 +72,21 @@ class GamifiedTextWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min, // Adjusts the size to fit children
children: [
const SizedBox(height: 10), // Spacing between the star and text
// Star animation above the text
const StarAnimationWidget(),
const SizedBox(height: 10), // Spacing between the star and text
Container(
constraints: const BoxConstraints(
minHeight: 80,
),
padding: const EdgeInsets.all(8),
child: Text(
return SizedBox(
width: AppConfig.toolbarMinWidth,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
child: Column(
children: [
const StarAnimationWidget(),
const SizedBox(height: 10),
Text(
userMessage,
style: BotStyle.text(context),
textAlign: TextAlign.center, // Center-align the text
textAlign: TextAlign.center,
),
),
],
],
),
),
);
}

View file

@ -4,16 +4,16 @@ import 'dart:developer';
import 'package:fluffychat/pangea/controllers/pangea_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/pangea_representation_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
import 'package:fluffychat/pangea/utils/bot_style.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/widgets/animations/gain_points.dart';
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart';
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
import 'package:fluffychat/pangea/widgets/content_issue_button.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/multiple_choice_activity.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/no_more_practice_card.dart';
@ -29,19 +29,20 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
class PracticeActivityCard extends StatefulWidget {
final PangeaMessageEvent pangeaMessageEvent;
final MessageOverlayController overlayController;
final TtsController tts;
const PracticeActivityCard({
super.key,
required this.pangeaMessageEvent,
required this.overlayController,
required this.tts,
});
@override
MessagePracticeActivityCardState createState() =>
MessagePracticeActivityCardState();
PracticeActivityCardState createState() => PracticeActivityCardState();
}
class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
class PracticeActivityCardState extends State<PracticeActivityCard> {
PracticeActivityModel? currentActivity;
PracticeActivityRecordModel? currentCompletionRecord;
bool fetchingActivity = false;
@ -119,13 +120,25 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
return null;
}
if (widget.pangeaMessageEvent.originalSent == null) {
debugger(when: kDebugMode);
_updateFetchingActivity(false);
ErrorHandler.logError(
e: Exception('No original message found in _fetchNewActivity'),
data: {
'event': widget.pangeaMessageEvent.event.toJson(),
},
);
return null;
}
final PracticeActivityModel? ourNewActivity = await pangeaController
.practiceGenerationController
.getPracticeActivity(
MessageActivityRequest(
userL1: pangeaController.languageController.userL1!.langCode,
userL2: pangeaController.languageController.userL2!.langCode,
messageText: representation!.text,
messageText: widget.pangeaMessageEvent.originalSent!.text,
tokensWithXP: await targetTokensController.targetTokens(
context,
widget.pangeaMessageEvent,
@ -164,13 +177,26 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
);
Future<void> _savorTheJoy() async {
debugger(when: savoringTheJoy && kDebugMode);
try {
debugger(when: savoringTheJoy && kDebugMode);
setState(() => savoringTheJoy = true);
if (mounted) setState(() => savoringTheJoy = true);
await Future.delayed(appropriateTimeForJoy);
await Future.delayed(appropriateTimeForJoy);
if (mounted) setState(() => savoringTheJoy = false);
if (mounted) setState(() => savoringTheJoy = false);
} catch (e, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(
e: e,
s: s,
m: 'Failed to savor the joy',
data: {
'activity': currentActivity,
'record': currentCompletionRecord,
},
);
}
}
/// Called when the user finishes an activity.
@ -200,7 +226,8 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
widget.pangeaMessageEvent.eventId,
);
//
// wait for the joy to be savored before resolving the activity
// and setting it to replace the previous activity
final Iterable<dynamic> result = await Future.wait([
_savorTheJoy(),
_fetchNewActivity(),
@ -256,27 +283,21 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
});
}
RepresentationEvent? get representation =>
widget.pangeaMessageEvent.originalSent;
String get messsageText => representation!.text;
PangeaController get pangeaController => MatrixState.pangeaController;
/// The widget that displays the current activity.
/// If there is no current activity, the widget returns a sizedbox with a height of 80.
/// If the activity type is multiple choice, the widget returns a MultipleChoiceActivity.
/// If the activity type is unknown, the widget logs an error and returns a text widget with an error message.
Widget get activityWidget {
if (currentActivity == null) {
// return sizedbox with height of 80
return const SizedBox(height: 80);
}
switch (currentActivity!.activityType) {
Widget? get activityWidget {
switch (currentActivity?.activityType) {
case null:
return null;
case ActivityTypeEnum.multipleChoice:
return MultipleChoiceActivity(
practiceCardController: this,
currentActivity: currentActivity!,
tts: widget.tts,
);
case ActivityTypeEnum.wordFocusListening:
// return WordFocusListeningActivity(
@ -284,70 +305,56 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
return MultipleChoiceActivity(
practiceCardController: this,
currentActivity: currentActivity!,
tts: widget.tts,
);
default:
ErrorHandler.logError(
e: Exception('Unknown activity type'),
m: 'Unknown activity type',
data: {
'activityType': currentActivity!.activityType,
},
);
return Text(
L10n.of(context)!.oopsSomethingWentWrong,
style: BotStyle.text(context),
);
// default:
// ErrorHandler.logError(
// e: Exception('Unknown activity type'),
// m: 'Unknown activity type',
// data: {
// 'activityType': currentActivity!.activityType,
// },
// );
// return Text(
// L10n.of(context)!.oopsSomethingWentWrong,
// style: BotStyle.text(context),
// );
}
}
String? get userMessage {
if (!fetchingActivity && currentActivity == null) {
return L10n.of(context)!.noActivitiesFound;
}
return null;
}
@override
Widget build(BuildContext context) {
if (userMessage != null) {
return GamifiedTextWidget(userMessage: userMessage!);
if (!fetchingActivity && currentActivity == null) {
return GamifiedTextWidget(
userMessage: L10n.of(context)!.noActivitiesFound,
);
}
return Row(
mainAxisSize: MainAxisSize.min,
return Stack(
alignment: Alignment.center,
children: [
Stack(
alignment: Alignment.center,
children: [
// Main content
const Positioned(
child: PointsGainedAnimation(),
),
Container(
padding: const EdgeInsets.all(8),
child: activityWidget,
),
// Conditionally show the darkening and progress indicator based on the loading state
if (!savoringTheJoy && fetchingActivity) ...[
// Semi-transparent overlay
Container(
color: Colors.black.withOpacity(0.5), // Darkening effect
),
// Circular progress indicator in the center
const Center(
child: CircularProgressIndicator(),
),
],
// Flag button in the top right corner
Positioned(
top: 0,
right: 0,
child: ContentIssueButton(
isActive: currentActivity != null,
submitFeedback: submitFeedback,
),
),
],
// Main content
const Positioned(
child: PointsGainedAnimation(),
),
if (activityWidget != null)
Padding(
padding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
child: activityWidget,
),
// Conditionally show the darkening and progress indicator based on the loading state
if (!savoringTheJoy && fetchingActivity) ...[
// Circular progress indicator in the center
const ToolbarContentLoadingIndicator(),
],
// Flag button in the top right corner
Positioned(
top: 0,
right: 0,
child: ContentIssueButton(
isActive: currentActivity != null,
submitFeedback: submitFeedback,
),
),
],
);

View file

@ -4,10 +4,12 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
class WordAudioButton extends StatefulWidget {
final String text;
final TtsController ttsController;
const WordAudioButton({
super.key,
required this.text,
required this.ttsController,
});
@override
@ -17,23 +19,9 @@ class WordAudioButton extends StatefulWidget {
class WordAudioButtonState extends State<WordAudioButton> {
bool _isPlaying = false;
TtsController ttsController = TtsController();
@override
void initState() {
// TODO: implement initState
super.initState();
ttsController.setupTTS().then((value) => setState(() {}));
}
@override
void dispose() {
ttsController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
debugPrint('build WordAudioButton');
return Column(
children: [
IconButton(
@ -52,7 +40,7 @@ class WordAudioButtonState extends State<WordAudioButton> {
_isPlaying ? L10n.of(context)!.stop : L10n.of(context)!.playAudio,
onPressed: () async {
if (_isPlaying) {
await ttsController.tts.stop();
await widget.ttsController.tts.stop();
if (mounted) {
setState(() => _isPlaying = false);
}
@ -60,14 +48,15 @@ class WordAudioButtonState extends State<WordAudioButton> {
if (mounted) {
setState(() => _isPlaying = true);
}
await ttsController.speak(widget.text);
await widget.ttsController.speak(widget.text);
if (mounted) {
setState(() => _isPlaying = false);
}
}
}, // Disable button if language isn't supported
),
ttsController.missingVoiceButton,
// #freeze-activity
widget.ttsController.missingVoiceButton,
],
);
}

View file

@ -13,7 +13,7 @@ import 'package:flutter/material.dart';
class WordFocusListeningActivity extends StatefulWidget {
final PracticeActivityModel activity;
final MessagePracticeActivityCardState practiceCardController;
final PracticeActivityCardState practiceCardController;
const WordFocusListeningActivity({
super.key,

View file

@ -10,18 +10,11 @@ class SelectToDefine extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
L10n.of(context)!.selectToDefine,
style: BotStyle.text(context),
textAlign: TextAlign.center,
),
),
],
padding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
child: Text(
L10n.of(context)!.selectToDefine,
style: BotStyle.text(context),
textAlign: TextAlign.center,
),
);
}

View file

@ -1,3 +1,4 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/utils/language_level_copy.dart';
import 'package:flutter/material.dart';
@ -19,18 +20,13 @@ class LanguageLevelDropdown extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DropdownButtonFormField(
// Initial Value
return DropdownButtonFormField2(
hint: Text(
L10n.of(context)!.selectLanguageLevel,
overflow: TextOverflow.clip,
textAlign: TextAlign.center,
),
value: initialLevel,
isExpanded: true,
// Down Arrow Icon
icon: const Icon(Icons.keyboard_arrow_down),
// Array list of items
items: LanguageLevelType.allInts.map((int levelOption) {
return DropdownMenuItem(
value: levelOption,

View file

@ -1,3 +1,4 @@
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
@ -11,18 +12,17 @@ class ErrorReporter {
void onErrorCallback(Object error, [StackTrace? stackTrace]) async {
Logs().e(message ?? 'Error caught', error, stackTrace);
// #Pangea
// Attempt to retrieve the L10n instance using the current context
final L10n? l10n = L10n.of(context);
// Check if the L10n instance is null
if (l10n == null) {
// Log an error message saying that the localization object is null
Logs().e('Localization object is null, cannot show error message.');
// Exits early to prevent further execution
return;
}
try {
// Attempt to retrieve the L10n instance using the current context
final L10n? l10n = L10n.of(context);
// Check if the L10n instance is null
if (l10n == null) {
// Log an error message saying that the localization object is null
Logs().e('Localization object is null, cannot show error message.');
// Exits early to prevent further execution
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
@ -32,6 +32,12 @@ class ErrorReporter {
);
} catch (err) {
debugPrint("Failed to show error snackbar.");
} finally {
ErrorHandler.logError(
e: error,
s: stackTrace,
m: message ?? 'Error caught',
);
}
}
// final text = '$error\n${stackTrace ?? ''}';

View file

@ -3,7 +3,6 @@ import 'dart:async';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart';
import 'package:fluffychat/pangea/utils/download_chat.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';
@ -154,27 +153,7 @@ class ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
case ChatPopupMenuActions.learningSettings:
showDialog(
context: context,
builder: (c) {
return kIsWeb
? Dialog(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 600,
maxHeight: 600,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20.0),
child: const SettingsLearning(isPopup: true),
),
),
)
: Dialog.fullscreen(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: const SettingsLearning(isPopup: true),
),
);
},
builder: (c) => const SettingsLearning(),
);
break;
// Pangea#

View file

@ -345,6 +345,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.0"
dropdown_button2:
dependency: "direct main"
description:
name: dropdown_button2
sha256: b0fe8d49a030315e9eef6c7ac84ca964250155a6224d491c1365061bc974a9e1
url: "https://pub.dev"
source: hosted
version: "2.3.9"
dynamic_color:
dependency: "direct main"
description:
@ -2642,14 +2650,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.1"
visibility_detector:
dependency: transitive
description:
name: visibility_detector
sha256: "15c54a459ec2c17b4705450483f3d5a2858e733aee893dcee9d75fd04814940d"
url: "https://pub.dev"
source: hosted
version: "0.3.3"
vm_service:
dependency: transitive
description:
@ -2723,7 +2723,7 @@ packages:
source: hosted
version: "1.2.0"
win32:
dependency: "direct overridden"
dependency: transitive
description:
name: win32
sha256: "015002c060f1ae9f41a818f2d5640389cc05283e368be19dc8d77cecb43c40c9"

View file

@ -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.21.5+3541
version: 1.22.6+3556
environment:
sdk: ">=3.0.0 <4.0.0"
@ -113,6 +113,7 @@ dependencies:
android_intent_plus: ^5.2.0
country_picker: ^2.0.25
csv: ^6.0.0
dropdown_button2: ^2.3.9
fl_chart: ^0.67.0
firebase_analytics: ^11.0.1
firebase_core: ^3.1.0