resolve merge conflicts
This commit is contained in:
commit
4d0b2a3df1
82 changed files with 2912 additions and 2727 deletions
|
|
@ -158,4 +158,10 @@
|
|||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.TTS_SERVICE" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -2812,7 +2812,7 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"unableToFindClass": "We are unable to find the space. Please double-check the information with the space administrator. If you are still experiencing an issue, please contact support@pangea.chat.",
|
||||
"unableToFindClass": "There's no space with that code. Please try again.",
|
||||
"@unableToFindClass": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
|
|
@ -3904,7 +3904,6 @@
|
|||
"listen": "Listen",
|
||||
"addConversationBot": "Enable Conversation Bot",
|
||||
"addConversationBotDesc": "Add a bot to this chat",
|
||||
"convoBotSettingsTitle": "Conversation Bot Settings",
|
||||
"convoBotSettingsDescription": "Edit conversation topic and difficulty",
|
||||
"enterAConversationTopic": "Enter a conversation topic",
|
||||
"conversationTopic": "Conversation topic",
|
||||
|
|
@ -4009,7 +4008,7 @@
|
|||
"accuracy": "Accuracy",
|
||||
"points": "Points",
|
||||
"noPaymentInfo": "No payment info necessary!",
|
||||
"conversationBotModeSelectDescription": "Bot mode",
|
||||
"conversationBotModeSelectDescription": "Chat Activity",
|
||||
"conversationBotModeSelectOption_discussion": "Discussion",
|
||||
"conversationBotModeSelectOption_custom": "Custom",
|
||||
"conversationBotModeSelectOption_conversation": "Conversation",
|
||||
|
|
@ -4030,7 +4029,7 @@
|
|||
"conversationBotCustomZone_customSystemPromptPlaceholder": "Set custom system prompt",
|
||||
"conversationBotCustomZone_customSystemPromptEmptyError": "Missing custom system prompt",
|
||||
"conversationBotCustomZone_customTriggerReactionEnabledLabel": "Responds on ⏩ reaction",
|
||||
"botConfig": "Conversation Bot Settings",
|
||||
"botConfig": "Chat Settings",
|
||||
"addConversationBotDialogTitleInvite": "Confirm inviting conversation bot",
|
||||
"addConversationBotButtonInvite": "Invite",
|
||||
"addConversationBotDialogInviteConfirmation": "Invite",
|
||||
|
|
@ -4038,7 +4037,7 @@
|
|||
"addConversationBotButtonRemove": "Remove",
|
||||
"addConversationBotDialogRemoveConfirmation": "Remove",
|
||||
"conversationBotConfigConfirmChange": "Confirm",
|
||||
"conversationBotStatus": "Bot Status",
|
||||
"conversationBotStatus": "Invite bot",
|
||||
"conversationBotTextAdventureZone_title": "Text Adventure",
|
||||
"conversationBotTextAdventureZone_instructionLabel": "Game Master Instructions",
|
||||
"conversationBotTextAdventureZone_instructionPlaceholder": "Set game master instructions",
|
||||
|
|
@ -4086,6 +4085,7 @@
|
|||
}
|
||||
},
|
||||
"roomCapacityExplanation": "{roomType} capacity limits the number of non-admins allowed in a room.",
|
||||
"tooManyRequest": "Too many request, please try again later.",
|
||||
"@roomCapacityExplanation": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
|
|
@ -4114,14 +4114,14 @@
|
|||
"placeholders": {}
|
||||
},
|
||||
"addChatToSpaceDesc": "Adding a chat to a space will make the chat appear within the space for students and give them access.",
|
||||
"addSpaceToSpaceDesc": "Adding a sub space to space will make the sub space appear in the main space's chat list.",
|
||||
"addSpaceToSpaceDesc": "Adding a sub space to space will make the sub space appear in the main space''s chat list.",
|
||||
"spaceAnalytics": "Space Analytics",
|
||||
"changeAnalyticsLanguage": "Change Analytics Language",
|
||||
"suggestToSpace": "Suggest this space",
|
||||
"suggestToSpaceDesc": "Suggested sub spaces will appear in their main space's chat list",
|
||||
"suggestToSpaceDesc": "Suggested sub spaces will appear in their main space''s chat list",
|
||||
"practice": "Practice",
|
||||
"noLanguagesSet": "No languages set",
|
||||
"noActivitiesFound": "That's enough on this for now! Come back later for more.",
|
||||
"noActivitiesFound": "That''s enough on this for now! Come back later for more.",
|
||||
"hintTitle": "Hint:",
|
||||
"speechToTextBody": "See how well you did by looking at your Accuracy and Words Per Minute scores",
|
||||
"previous": "Previous",
|
||||
|
|
@ -4225,17 +4225,144 @@
|
|||
"discoverHomeservers": "Discover homeservers",
|
||||
"whatIsAHomeserver": "What is a homeserver?",
|
||||
"homeserverDescription": "All your data is stored on the homeserver, just like an email provider. You can choose which homeserver you want to use, while you can still communicate with everyone. Learn more at at https://matrix.org.",
|
||||
"doesNotSeemToBeAValidHomeserver": "Doesn't seem to be a compatible homeserver. Wrong URL?",
|
||||
"doesNotSeemToBeAValidHomeserver": "Doesn''t seem to be a compatible homeserver. Wrong URL?",
|
||||
"grammar": "Grammar",
|
||||
"contactHasBeenInvitedToTheChat": "Contact has been invited to the chat",
|
||||
"inviteChat": "📨 Invite chat",
|
||||
"chatName": "Chat name",
|
||||
"reportContentIssueTitle": "Report content issue",
|
||||
"feedback": "Optional feedback",
|
||||
"reportContentIssueDescription": "Uh oh! AI can faciliate personalized learning experiences but... also hallucinates. Please provide any feedback you have and we'll try again.",
|
||||
"reportContentIssueDescription": "Uh oh! AI can faciliate personalized learning experiences but... also hallucinates. Please provide any feedback you have and we''ll try again.",
|
||||
"clickTheWordAgainToDeselect": "Click the selected word to deselect it.",
|
||||
"l2SupportNa": "Not Available",
|
||||
"l2SupportAlpha": "Alpha",
|
||||
"l2SupportBeta": "Beta",
|
||||
"l2SupportFull": "Full"
|
||||
"l2SupportFull": "Full",
|
||||
"voiceNotAvailable": "It looks like you don't have a voice installed for this language.",
|
||||
"openVoiceSettings": "Click here to open voice settings",
|
||||
"playAudio": "Play",
|
||||
"stop": "Stop",
|
||||
"grammarCopySCONJ": "Subordinating Conjunction",
|
||||
"grammarCopyNUM": "Number",
|
||||
"grammarCopyVERB": "Verb",
|
||||
"grammarCopyAFFIX": "Affix",
|
||||
"grammarCopyPARTpos": "Particle",
|
||||
"grammarCopyADJ": "Adjective",
|
||||
"grammarCopyCCONJ": "Coordinating Conjunction",
|
||||
"grammarCopyPUNCT": "Punctuation",
|
||||
"grammarCopyADV": "Adverb",
|
||||
"grammarCopyAUX": "Auxiliary",
|
||||
"grammarCopySPACE": "Space",
|
||||
"grammarCopySYM": "Symbol",
|
||||
"grammarCopyDET": "Determiner",
|
||||
"grammarCopyPRON": "Pronoun",
|
||||
"grammarCopyADP": "Adposition",
|
||||
"grammarCopyPROPN": "Proper Noun",
|
||||
"grammarCopyNOUN": "Noun",
|
||||
"grammarCopyINTJ": "Interjection",
|
||||
"grammarCopyX": "Other",
|
||||
"grammarCopyFem": "Feminine",
|
||||
"grammarCopy2": "Second Person",
|
||||
"grammarCopyImp": "Imperative",
|
||||
"grammarCopyQest": "Question",
|
||||
"grammarCopyPerf": "Perfect",
|
||||
"grammarCopyAccNom": "Accusative, Nominative",
|
||||
"grammarCopyObl": "Oblique Case",
|
||||
"grammarCopyAct": "Active",
|
||||
"grammarCopyBrck": "Bracket",
|
||||
"grammarCopyArt": "Article",
|
||||
"grammarCopySing": "Singular",
|
||||
"grammarCopyMasc": "Masculine",
|
||||
"grammarCopyMod": "Modal",
|
||||
"grammarCopyAdverbial": "Adverbial",
|
||||
"grammarCopyPeri": "Periphrastic",
|
||||
"grammarCopyDigit": "Digit",
|
||||
"grammarCopyNot_proper": "Not Proper",
|
||||
"grammarCopyCard": "Cardinal",
|
||||
"grammarCopyProp": "Proper",
|
||||
"grammarCopyDash": "Dash",
|
||||
"grammarCopyYes": "Yes",
|
||||
"grammarCopySemi": "Semicolon",
|
||||
"grammarCopyComm": "Comma",
|
||||
"grammarCopyCnd": "Conditional",
|
||||
"grammarCopyIntRel": "Interrogative, Relative",
|
||||
"grammarCopyAcc": "Accusative",
|
||||
"grammarCopyPartTag": "Partitive",
|
||||
"grammarCopyInt": "Interrogative",
|
||||
"grammarCopyPast": "Past",
|
||||
"grammarCopySup": "Superlative",
|
||||
"grammarCopyColo": "Colon",
|
||||
"grammarCopy3": "Third Person",
|
||||
"grammarCopyPlur": "Plural",
|
||||
"grammarCopyNpr": "Proper Noun",
|
||||
"grammarCopyInterrogative": "Interrogative",
|
||||
"grammarCopyInfm": "Informal",
|
||||
"grammarCopyTim": "Time",
|
||||
"grammarCopyNeg": "Negative",
|
||||
"grammarCopyTot": "Total",
|
||||
"grammarCopyAdnomial": "Adnominal",
|
||||
"grammarCopyProg": "Progressive",
|
||||
"grammarCopySub": "Subjunctive",
|
||||
"grammarCopyComplementive": "Complementive",
|
||||
"grammarCopyNom": "Nominative",
|
||||
"grammarCopyFut": "Future",
|
||||
"grammarCopyDat": "Dative",
|
||||
"grammarCopyPres": "Present",
|
||||
"grammarCopyNeut": "Neuter",
|
||||
"grammarCopyRel": "Relative",
|
||||
"grammarCopyFinal_ending": "Final Ending",
|
||||
"grammarCopyDem": "Demonstrative",
|
||||
"grammarCopyPre": "Preposition",
|
||||
"grammarCopyFin": "Finite",
|
||||
"grammarCopyPos": "Positive",
|
||||
"grammarCopyQuot": "Quotation",
|
||||
"grammarCopyGer": "Gerund",
|
||||
"grammarCopyPass": "Passive",
|
||||
"grammarCopyGen": "Genitive",
|
||||
"grammarCopyPrs": "Present",
|
||||
"grammarCopyDef": "Definite",
|
||||
"grammarCopyOrd": "Ordinal",
|
||||
"grammarCopyIns": "Instrumental",
|
||||
"grammarCopyAccDat": "Accusative, Dative",
|
||||
"grammarCopyInf": "Infinitive",
|
||||
"grammarCopyLong": "Long",
|
||||
"grammarCopyInd": "Indicative",
|
||||
"grammarCopyCmp": "Comparative",
|
||||
"grammarCopyRelative_case": "Relative Case",
|
||||
"grammarCopyExcl": "Exclamative",
|
||||
"grammarCopy1": "First Person",
|
||||
"grammarCopyIni": "Initial",
|
||||
"grammarCopyPerson": "Person",
|
||||
"grammarCopyForeign": "Foreign",
|
||||
"grammarCopyVoice": "Voice",
|
||||
"grammarCopyVerbType": "Verb Type",
|
||||
"grammarCopyPoss": "Possessive",
|
||||
"grammarCopyPrepCase": "Prepositional Case",
|
||||
"grammarCopyNumType": "Numeral Type",
|
||||
"grammarCopyNounType": "Noun Type",
|
||||
"grammarCopyReflex": "Reflexive",
|
||||
"grammarCopyPronType": "Pronoun Type",
|
||||
"grammarCopyPunctSide": "Punctuation Side",
|
||||
"grammarCopyVerbForm": "Verb Form",
|
||||
"grammarCopyGender": "Gender",
|
||||
"grammarCopyMood": "Mood",
|
||||
"grammarCopyAspect": "Aspect",
|
||||
"grammarCopyPunctType": "Punctuation Type",
|
||||
"grammarCopyTense": "Tense",
|
||||
"grammarCopyDegree": "Degree",
|
||||
"grammarCopyPolite": "Politeness",
|
||||
"grammarCopyAdvType": "Adverb Type",
|
||||
"grammarCopyNumber": "Number",
|
||||
"grammarCopyConjType": "Conjunction Type",
|
||||
"grammarCopyPolarity": "Polarity",
|
||||
"grammarCopyNumberPsor": "Possessor''s Number",
|
||||
"grammarCopyCase": "Case",
|
||||
"grammarCopyDefinite": "Definiteness",
|
||||
"grammarCopyNumForm": "Numeral Form",
|
||||
"grammarCopyUnknown": "Unknown",
|
||||
"enterPrompt": "Please enter a system prompt",
|
||||
"selectBotLanguage": "Select bot language",
|
||||
"chooseVoice": "Choose a voice",
|
||||
"enterLanguageLevel": "Please enter a language level",
|
||||
"enterDiscussionTopic": "Please enter a discussion topic"
|
||||
}
|
||||
|
|
@ -4244,7 +4244,7 @@
|
|||
"joinWithClassCode": "Únete a una clase o a un intercambio",
|
||||
"joinWithClassCodeDesc": "Conéctese a una clase o espacio de intercambio con el código de invitación de 6 dígitos proporcionado por el administrador del espacio.",
|
||||
"joinWithClassCodeHint": "Introduzca el código de invitación",
|
||||
"unableToFindClass": "No podemos encontrar la clase o el intercambio. Por favor, vuelva a comprobar la información con el administrador del espacio. Si sigue teniendo problemas, póngase en contacto con support@pangea.chat.",
|
||||
"unableToFindClass": "No hay espacio con ese código. Por favor inténtalo de nuevo.",
|
||||
"welcomeToYourNewClass": "Bienvenido 🙂",
|
||||
"welcomeToClass": "Bienvenido! 🙂\n- ¡Prueba a unirte a un chat!\n- ¡Diviértete chateando!",
|
||||
"unableToFindClassCode": "No se puede encontrar el código.",
|
||||
|
|
|
|||
|
|
@ -233,11 +233,7 @@ abstract class AppRoutes {
|
|||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
NewGroup(
|
||||
// #Pangea
|
||||
spaceId: state.uri.queryParameters['spaceId'],
|
||||
// Pangea#
|
||||
),
|
||||
const NewGroup(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
// #Pangea
|
||||
|
|
|
|||
|
|
@ -560,6 +560,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
//#Pangea
|
||||
choreographer.stateListener.close();
|
||||
choreographer.dispose();
|
||||
clearSelectedEvents();
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
//Pangea#
|
||||
super.dispose();
|
||||
|
|
@ -1334,13 +1335,18 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
}
|
||||
// Pangea#
|
||||
|
||||
void clearSelectedEvents() => setState(() {
|
||||
// #Pangea
|
||||
closeSelectionOverlay();
|
||||
// Pangea#
|
||||
selectedEvents.clear();
|
||||
showEmojiPicker = false;
|
||||
});
|
||||
void clearSelectedEvents() {
|
||||
// #Pangea
|
||||
if (!mounted) return;
|
||||
// Pangea#
|
||||
setState(() {
|
||||
// #Pangea
|
||||
closeSelectionOverlay();
|
||||
// Pangea#
|
||||
selectedEvents.clear();
|
||||
showEmojiPicker = false;
|
||||
});
|
||||
}
|
||||
|
||||
void clearSingleSelectedEvent() {
|
||||
if (selectedEvents.length <= 1) {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,13 @@ class AudioPlayerWidget extends StatefulWidget {
|
|||
|
||||
static String? currentId;
|
||||
|
||||
static const int wavesCount = 40;
|
||||
// #Pangea
|
||||
// static const int wavesCount = 40;
|
||||
static const int wavesCount = kIsWeb ? 100 : 40;
|
||||
|
||||
final int? sectionStartMS;
|
||||
final int? sectionEndMS;
|
||||
// Pangea#
|
||||
|
||||
const AudioPlayerWidget(
|
||||
this.event, {
|
||||
|
|
@ -33,6 +39,8 @@ class AudioPlayerWidget extends StatefulWidget {
|
|||
// #Pangea
|
||||
this.matrixFile,
|
||||
this.autoplay = false,
|
||||
this.sectionStartMS,
|
||||
this.sectionEndMS,
|
||||
// Pangea#
|
||||
super.key,
|
||||
});
|
||||
|
|
@ -72,6 +80,24 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
// #Pangea
|
||||
// @override
|
||||
// void didUpdateWidget(covariant oldWidget) {
|
||||
// if ((oldWidget.sectionEndMS != widget.sectionEndMS) ||
|
||||
// (oldWidget.sectionStartMS != widget.sectionStartMS)) {
|
||||
// debugPrint('selection changed');
|
||||
// if (widget.sectionStartMS != null) {
|
||||
// audioPlayer?.seek(Duration(milliseconds: widget.sectionStartMS!));
|
||||
// audioPlayer?.play();
|
||||
// } else {
|
||||
// audioPlayer?.stop();
|
||||
// audioPlayer?.seek(null);
|
||||
// }
|
||||
// }
|
||||
// super.didUpdateWidget(oldWidget);
|
||||
// }
|
||||
// Pangea#
|
||||
|
||||
Future<void> _downloadAction() async {
|
||||
// #Pangea
|
||||
// if (status != AudioPlayerStatus.notDownloaded) return;
|
||||
|
|
@ -160,7 +186,16 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
AudioPlayerWidget.wavesCount)
|
||||
.round();
|
||||
});
|
||||
// #Pangea
|
||||
// if (widget.sectionStartMS != null &&
|
||||
// widget.sectionEndMS != null &&
|
||||
// state.inMilliseconds.toDouble() >= widget.sectionEndMS!) {
|
||||
// audioPlayer.stop();
|
||||
// audioPlayer.seek(Duration(milliseconds: widget.sectionStartMS!));
|
||||
// } else
|
||||
if (state.inMilliseconds.toDouble() == maxPosition) {
|
||||
// if (state.inMilliseconds.toDouble() == maxPosition) {
|
||||
// Pangea#
|
||||
audioPlayer.stop();
|
||||
audioPlayer.seek(null);
|
||||
}
|
||||
|
|
@ -194,6 +229,11 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
}
|
||||
// Pangea#
|
||||
}
|
||||
// #Pangea
|
||||
// if (widget.sectionStartMS != null) {
|
||||
// audioPlayer.seek(Duration(milliseconds: widget.sectionStartMS!));
|
||||
// }
|
||||
// Pangea#
|
||||
audioPlayer.play().onError(
|
||||
ErrorReporter(context, 'Unable to play audio message')
|
||||
.onErrorCallback,
|
||||
|
|
@ -311,6 +351,17 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
|
||||
final statusText = this.statusText ??= _durationString ?? '00:00';
|
||||
final audioPlayer = this.audioPlayer;
|
||||
|
||||
// #Pangea
|
||||
final msPerWave = (maxPosition / AudioPlayerWidget.wavesCount);
|
||||
final int? startWave = widget.sectionStartMS != null && msPerWave > 0
|
||||
? (widget.sectionStartMS! / msPerWave).floor()
|
||||
: null;
|
||||
final int? endWave = widget.sectionEndMS != null && msPerWave > 0
|
||||
? (widget.sectionEndMS! / msPerWave).ceil()
|
||||
: null;
|
||||
// Pangea#
|
||||
|
||||
return Padding(
|
||||
// #Pangea
|
||||
// padding: const EdgeInsets.all(12.0),
|
||||
|
|
@ -352,44 +403,98 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
// #Pangea
|
||||
// const SizedBox(width: 8),
|
||||
const SizedBox(width: 5),
|
||||
// Pangea#
|
||||
// Row(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// for (var i = 0; i < AudioPlayerWidget.wavesCount; i++)
|
||||
// GestureDetector(
|
||||
// onTapDown: (_) => audioPlayer?.seek(
|
||||
// Duration(
|
||||
// milliseconds:
|
||||
// (maxPosition / AudioPlayerWidget.wavesCount).round() *
|
||||
// i,
|
||||
// ),
|
||||
// ),
|
||||
// child: Container(
|
||||
// height: 32,
|
||||
// color: widget.color.withAlpha(0),
|
||||
// alignment: Alignment.center,
|
||||
// child: Opacity(
|
||||
// opacity: currentPosition > i ? 1 : 0.5,
|
||||
// child: Container(
|
||||
// margin: const EdgeInsets.symmetric(horizontal: 1),
|
||||
// decoration: BoxDecoration(
|
||||
// color: widget.color,
|
||||
// borderRadius: BorderRadius.circular(2),
|
||||
// ),
|
||||
// // #Pangea
|
||||
// // width: 2,
|
||||
// width: 1,
|
||||
// // Pangea#
|
||||
// height: 32 * (waveform[i] / 1024),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// const SizedBox(width: 8),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (var i = 0; i < AudioPlayerWidget.wavesCount; i++)
|
||||
GestureDetector(
|
||||
onTapDown: (_) => audioPlayer?.seek(
|
||||
Duration(
|
||||
milliseconds:
|
||||
(maxPosition / AudioPlayerWidget.wavesCount).round() *
|
||||
i,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
height: 32,
|
||||
color: widget.color.withAlpha(0),
|
||||
alignment: Alignment.center,
|
||||
child: Opacity(
|
||||
opacity: currentPosition > i ? 1 : 0.5,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.color,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
// #Pangea
|
||||
// width: 2,
|
||||
width: 1,
|
||||
// Pangea#
|
||||
height: 32 * (waveform[i] / 1024),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final double barOpacity = currentPosition > i ? 1 : 0.5;
|
||||
return GestureDetector(
|
||||
onTapDown: (_) {
|
||||
audioPlayer?.seek(
|
||||
Duration(
|
||||
milliseconds:
|
||||
(maxPosition / AudioPlayerWidget.wavesCount)
|
||||
.round() *
|
||||
i,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 0.5,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.color.withOpacity(barOpacity),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
height: 32 * (waveform[i] / 1024),
|
||||
width: 1.5,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
// return Container(
|
||||
// height: 32,
|
||||
// width: 2,
|
||||
// alignment: Alignment.center,
|
||||
// child: Opacity(
|
||||
// opacity: barOpacity,
|
||||
// child: Container(
|
||||
// margin: const EdgeInsets.symmetric(
|
||||
// horizontal: 1,
|
||||
// ),
|
||||
// decoration: BoxDecoration(
|
||||
// color: widget.color,
|
||||
// borderRadius: BorderRadius.circular(2),
|
||||
// ),
|
||||
// height: 32 * (waveform[i] / 1024),
|
||||
// width: 2,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
// #Pangea
|
||||
// const SizedBox(width: 8),
|
||||
const SizedBox(width: 5),
|
||||
// SizedBox(
|
||||
// width: 36,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import 'package:fluffychat/pages/chat_details/chat_details_view.dart';
|
|||
import 'package:fluffychat/pages/settings/settings.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_description_button.dart';
|
||||
import 'package:fluffychat/pangea/utils/set_class_name.dart';
|
||||
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/app_lock.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -41,9 +40,7 @@ class ChatDetailsController extends State<ChatDetails> {
|
|||
String? get roomId => widget.roomId;
|
||||
|
||||
// #Pangea
|
||||
final GlobalKey<AddToSpaceState> addToSpaceKey = GlobalKey<AddToSpaceState>();
|
||||
final GlobalKey<ChatDetailsController>
|
||||
addConversationBotKey =
|
||||
final GlobalKey<ChatDetailsController> addConversationBotKey =
|
||||
GlobalKey<ChatDetailsController>();
|
||||
|
||||
bool displayAddStudentOptions = false;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_inv
|
|||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_name_button.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart';
|
||||
import 'package:fluffychat/pangea/utils/lock_room.dart';
|
||||
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
|
||||
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings.dart';
|
||||
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
|
|
@ -395,71 +394,6 @@ class ChatDetailsView extends StatelessWidget {
|
|||
room: room,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
if (!room.isDirectChat && room.isRoomAdmin)
|
||||
AddToSpaceToggles(
|
||||
roomId: room.id,
|
||||
key: controller.addToSpaceKey,
|
||||
startOpen: false,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
if (!room.isDirectChat)
|
||||
if (room.isRoomAdmin)
|
||||
ListTile(
|
||||
title: Text(
|
||||
room.isSpace
|
||||
? L10n.of(context)!.archiveSpace
|
||||
: L10n.of(context)!.archive,
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: iconColor,
|
||||
child: const Icon(
|
||||
Icons.archive_outlined,
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
var confirmed = OkCancelResult.ok;
|
||||
var shouldGo = false;
|
||||
// archiveSpace has its own popup; only show if not space
|
||||
if (!room.isSpace) {
|
||||
confirmed = await showOkCancelAlertDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context)!.areYouSure,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
message: L10n.of(context)!
|
||||
.archiveRoomDescription,
|
||||
);
|
||||
}
|
||||
if (confirmed == OkCancelResult.ok) {
|
||||
if (room.isSpace) {
|
||||
shouldGo = await room.archiveSpace(
|
||||
context,
|
||||
Matrix.of(context).client,
|
||||
);
|
||||
} else {
|
||||
final success =
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
await room.archive();
|
||||
},
|
||||
);
|
||||
shouldGo = (success.error == null);
|
||||
}
|
||||
if (shouldGo) {
|
||||
context.go('/rooms');
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.leave,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||
|
||||
return SliverAppBar(
|
||||
floating: true,
|
||||
toolbarHeight: 175,
|
||||
// #Pangea
|
||||
// toolbarHeight: 72,
|
||||
toolbarHeight: controller.isSearchMode ? 72 : 175,
|
||||
// Pangea#
|
||||
pinned:
|
||||
FluffyThemes.isColumnMode(context) || selectMode != SelectMode.normal,
|
||||
scrolledUnderElevation: selectMode == SelectMode.normal ? 0 : null,
|
||||
|
|
@ -32,111 +35,124 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||
// selectMode == SelectMode.normal ? Colors.transparent : null,
|
||||
// Pangea#
|
||||
automaticallyImplyLeading: false,
|
||||
leading: selectMode == SelectMode.normal
|
||||
? null
|
||||
: IconButton(
|
||||
tooltip: L10n.of(context)!.cancel,
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: controller.cancelAction,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
title: selectMode == SelectMode.share
|
||||
? Text(
|
||||
L10n.of(context)!.share,
|
||||
key: const ValueKey(SelectMode.share),
|
||||
)
|
||||
// #Pangea
|
||||
: Column(
|
||||
children: [
|
||||
ClientChooserButton(controller),
|
||||
const LearningProgressIndicators(),
|
||||
],
|
||||
),
|
||||
// : TextField(
|
||||
// controller: controller.searchController,
|
||||
// focusNode: controller.searchFocusNode,
|
||||
// textInputAction: TextInputAction.search,
|
||||
// onChanged: (text) => controller.onSearchEnter(
|
||||
// text,
|
||||
// globalSearch: globalSearch,
|
||||
// ),
|
||||
// decoration: InputDecoration(
|
||||
// filled: true,
|
||||
// fillColor: theme.colorScheme.secondaryContainer,
|
||||
// border: OutlineInputBorder(
|
||||
// borderSide: BorderSide.none,
|
||||
// borderRadius: BorderRadius.circular(99),
|
||||
// #Pangea
|
||||
// leading: selectMode == SelectMode.normal
|
||||
// ? null
|
||||
// : IconButton(
|
||||
// tooltip: L10n.of(context)!.cancel,
|
||||
// icon: const Icon(Icons.close_outlined),
|
||||
// onPressed: controller.cancelAction,
|
||||
// color: theme.colorScheme.primary,
|
||||
// ),
|
||||
// contentPadding: EdgeInsets.zero,
|
||||
// hintText: L10n.of(context)!.searchChatsRooms,
|
||||
// hintStyle: TextStyle(
|
||||
// color: theme.colorScheme.onPrimaryContainer,
|
||||
// fontWeight: FontWeight.normal,
|
||||
// ),
|
||||
// floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
// prefixIcon: controller.isSearchMode
|
||||
// ? IconButton(
|
||||
// tooltip: L10n.of(context)!.cancel,
|
||||
// icon: const Icon(Icons.close_outlined),
|
||||
// onPressed: controller.cancelSearch,
|
||||
// color: theme.colorScheme.onPrimaryContainer,
|
||||
// )
|
||||
// : IconButton(
|
||||
// onPressed: controller.startSearch,
|
||||
// icon: Icon(
|
||||
// Icons.search_outlined,
|
||||
// color: theme.colorScheme.onPrimaryContainer,
|
||||
// ),
|
||||
// ),
|
||||
// suffixIcon: controller.isSearchMode && globalSearch
|
||||
// ? controller.isSearching
|
||||
// ? const Padding(
|
||||
// padding: EdgeInsets.symmetric(
|
||||
// vertical: 10.0,
|
||||
// horizontal: 12,
|
||||
// ),
|
||||
// child: SizedBox.square(
|
||||
// dimension: 24,
|
||||
// child: CircularProgressIndicator.adaptive(
|
||||
// strokeWidth: 2,
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// : TextButton.icon(
|
||||
// onPressed: controller.setServer,
|
||||
// style: TextButton.styleFrom(
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(99),
|
||||
// ),
|
||||
// textStyle: const TextStyle(fontSize: 12),
|
||||
// ),
|
||||
// icon: const Icon(Icons.edit_outlined, size: 16),
|
||||
// label: Text(
|
||||
// controller.searchServer ??
|
||||
// Matrix.of(context).client.homeserver!.host,
|
||||
// maxLines: 2,
|
||||
// ),
|
||||
// )
|
||||
// : SizedBox(
|
||||
// width: 0,
|
||||
// child: ClientChooserButton(controller),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Pangea#
|
||||
actions: selectMode == SelectMode.share
|
||||
? [
|
||||
// #Pangea
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.symmetric(
|
||||
// horizontal: 16.0,
|
||||
// vertical: 8.0,
|
||||
// ),
|
||||
// child: ClientChooserButton(controller),
|
||||
// ),
|
||||
// Pangea#
|
||||
]
|
||||
: null,
|
||||
title:
|
||||
// #Pangea
|
||||
// selectMode == SelectMode.share
|
||||
// ? Text(
|
||||
// L10n.of(context)!.share,
|
||||
// key: const ValueKey(SelectMode.share),
|
||||
// )
|
||||
// :
|
||||
// Pangea#
|
||||
Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: controller.searchController,
|
||||
focusNode: controller.searchFocusNode,
|
||||
textInputAction: TextInputAction.search,
|
||||
onChanged: (text) => controller.onSearchEnter(
|
||||
text,
|
||||
globalSearch: globalSearch,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: theme.colorScheme.secondaryContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
hintText: L10n.of(context)!.searchChatsRooms,
|
||||
hintStyle: TextStyle(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
prefixIcon: controller.isSearchMode
|
||||
? IconButton(
|
||||
tooltip: L10n.of(context)!.cancel,
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: controller.cancelSearch,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
: IconButton(
|
||||
onPressed: controller.startSearch,
|
||||
icon: Icon(
|
||||
Icons.search_outlined,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
suffixIcon: controller.isSearchMode && globalSearch
|
||||
? controller.isSearching
|
||||
? const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 10.0,
|
||||
horizontal: 12,
|
||||
),
|
||||
child: SizedBox.square(
|
||||
dimension: 24,
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
)
|
||||
// #Pangea
|
||||
: SizedBox(
|
||||
width: 0,
|
||||
child: ClientChooserButton(controller),
|
||||
)
|
||||
// : TextButton.icon(
|
||||
// onPressed: controller.setServer,
|
||||
// style: TextButton.styleFrom(
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(99),
|
||||
// ),
|
||||
// textStyle: const TextStyle(fontSize: 12),
|
||||
// ),
|
||||
// icon: const Icon(Icons.edit_outlined, size: 16),
|
||||
// label: Text(
|
||||
// controller.searchServer ??
|
||||
// Matrix.of(context).client.homeserver!.host,
|
||||
// maxLines: 2,
|
||||
// ),
|
||||
// )
|
||||
// Pangea#
|
||||
: SizedBox(
|
||||
width: 0,
|
||||
child: ClientChooserButton(controller),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!controller.isSearchMode)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 16.0),
|
||||
child: LearningProgressIndicators(),
|
||||
),
|
||||
],
|
||||
),
|
||||
// #Pangea
|
||||
// actions: selectMode == SelectMode.share
|
||||
// ? [
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.symmetric(
|
||||
// horizontal: 16.0,
|
||||
// vertical: 8.0,
|
||||
// ),
|
||||
// child: ClientChooserButton(controller),
|
||||
// ),
|
||||
// ]
|
||||
// : null,
|
||||
// Pangea#
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:fluffychat/pangea/utils/find_conversation_partner_dialog.dart';
|
||||
import 'package:fluffychat/pangea/utils/logout.dart';
|
||||
import 'package:fluffychat/pangea/utils/space_code.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
// import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../utils/fluffy_share.dart';
|
||||
import 'chat_list.dart';
|
||||
|
||||
class ClientChooserButton extends StatelessWidget {
|
||||
|
|
@ -41,45 +41,30 @@ class ClientChooserButton extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
// PopupMenuItem(
|
||||
// enabled: matrix.client.rooms.any(
|
||||
// (room) =>
|
||||
// room.isSpace &&
|
||||
// room.ownPowerLevel >= ClassDefaultValues.powerLevelOfAdmin,
|
||||
// ),
|
||||
// value: SettingsAction.spaceAnalytics,
|
||||
// child: Row(
|
||||
// children: [
|
||||
// const Icon(Icons.analytics_outlined),
|
||||
// const SizedBox(width: 18),
|
||||
// Expanded(child: Text(L10n.of(context)!.spaceAnalytics)),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// enabled: matrix.client.rooms.any(
|
||||
// (room) => !room.isSpace && !room.isArchived && !room.isAnalyticsRoom,
|
||||
// ),
|
||||
// value: SettingsAction.myAnalytics,
|
||||
// child: Row(
|
||||
// children: [
|
||||
// const Icon(Icons.analytics_outlined),
|
||||
// const SizedBox(width: 18),
|
||||
// Expanded(child: Text(L10n.of(context)!.myLearning)),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: SettingsAction.newGroup,
|
||||
// child: Row(
|
||||
// children: [
|
||||
// const Icon(Icons.group_add_outlined),
|
||||
// const SizedBox(width: 18),
|
||||
// Text(L10n.of(context)!.createGroup),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.learning,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.psychology_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Expanded(child: Text(L10n.of(context)!.learningSettings)),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 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(
|
||||
|
|
@ -87,7 +72,7 @@ class ClientChooserButton extends StatelessWidget {
|
|||
const Icon(Icons.workspaces_outlined),
|
||||
const SizedBox(width: 18),
|
||||
// #Pangea
|
||||
Expanded(child: Text(L10n.of(context)!.createNewSpace)),
|
||||
Text(L10n.of(context)!.createNewSpace),
|
||||
// Text(L10n.of(context)!.createNewSpace),
|
||||
// Pangea#
|
||||
],
|
||||
|
|
@ -123,22 +108,10 @@ class ClientChooserButton extends StatelessWidget {
|
|||
children: [
|
||||
const Icon(Icons.archive_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Text(L10n.of(context)!.archive),
|
||||
Text(L10n.of(context)!!.archive),
|
||||
],
|
||||
),
|
||||
),*/
|
||||
// #Pangea
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.learning,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.psychology_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Expanded(child: Text(L10n.of(context)!.learningSettings)),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Pangea#
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.settings,
|
||||
child: Row(
|
||||
|
|
@ -146,13 +119,23 @@ class ClientChooserButton extends StatelessWidget {
|
|||
const Icon(Icons.settings_outlined),
|
||||
const SizedBox(width: 18),
|
||||
// #Pangea
|
||||
Text(L10n.of(context)!.settings),
|
||||
// Text(L10n.of(context)!.settings),
|
||||
Expanded(child: Text(L10n.of(context)!.settings)),
|
||||
// Pangea#
|
||||
],
|
||||
),
|
||||
),
|
||||
// #Pangea
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.logout,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.logout_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Expanded(child: Text(L10n.of(context)!.logout)),
|
||||
],
|
||||
),
|
||||
),
|
||||
// const PopupMenuDivider(),
|
||||
// for (final bundle in bundles) ...[
|
||||
// if (matrix.accountBundles[bundle]!.length != 1 ||
|
||||
|
|
@ -223,16 +206,6 @@ class ClientChooserButton extends StatelessWidget {
|
|||
// ],
|
||||
// ),
|
||||
// ),
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.logout,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.logout_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Expanded(child: Text(L10n.of(context)!.logout)),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Pangea#
|
||||
];
|
||||
}
|
||||
|
|
@ -281,32 +254,18 @@ class ClientChooserButton extends StatelessWidget {
|
|||
// onKeysPressed: () => _previousAccount(matrix, context),
|
||||
// child: const SizedBox.shrink(),
|
||||
// ),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
// Pangea#
|
||||
PopupMenuButton<Object>(
|
||||
onSelected: (o) => _clientSelected(o, context),
|
||||
itemBuilder: _bundleMenuItems,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child:
|
||||
// Pangea#
|
||||
PopupMenuButton<Object>(
|
||||
onSelected: (o) => _clientSelected(o, context),
|
||||
itemBuilder: _bundleMenuItems,
|
||||
// #Pangea
|
||||
child: ListTile(
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
leading: const Icon(Icons.settings_outlined),
|
||||
title: Text(L10n.of(context)!.mainMenu),
|
||||
),
|
||||
// child: Material(
|
||||
// color: Colors.transparent,
|
||||
// borderRadius: BorderRadius.circular(99),
|
||||
// child: Avatar(
|
||||
// mxContent: snapshot.data?.avatarUrl,
|
||||
// name: snapshot.data?.displayName ??
|
||||
// matrix.client.userID!.localpart,
|
||||
// size: 32,
|
||||
// ),
|
||||
// ),
|
||||
// Pangea#
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
child: Avatar(
|
||||
mxContent: snapshot.data?.avatarUrl,
|
||||
name: snapshot.data?.displayName ??
|
||||
matrix.client.userID!.localpart,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -353,42 +312,30 @@ class ClientChooserButton extends StatelessWidget {
|
|||
case SettingsAction.newSpace:
|
||||
controller.createNewSpace();
|
||||
break;
|
||||
case SettingsAction.invite:
|
||||
FluffyShare.shareInviteLink(context);
|
||||
break;
|
||||
// #Pangea
|
||||
// case SettingsAction.invite:
|
||||
// FluffyShare.shareInviteLink(context);
|
||||
// break;
|
||||
// Pangea#
|
||||
case SettingsAction.settings:
|
||||
context.go('/rooms/settings');
|
||||
break;
|
||||
case SettingsAction.archive:
|
||||
context.go('/rooms/archive');
|
||||
break;
|
||||
case SettingsAction.setStatus:
|
||||
controller.setStatus();
|
||||
// #Pangea
|
||||
// case SettingsAction.archive:
|
||||
// context.go('/rooms/archive');
|
||||
// break;
|
||||
// case SettingsAction.setStatus:
|
||||
// controller.setStatus();
|
||||
// break;
|
||||
case SettingsAction.learning:
|
||||
context.go('/rooms/settings/learning');
|
||||
break;
|
||||
case SettingsAction.newClass:
|
||||
context.go('/rooms/newspace');
|
||||
break;
|
||||
case SettingsAction.joinWithClassCode:
|
||||
SpaceCodeUtil.joinWithSpaceCodeDialog(
|
||||
context,
|
||||
MatrixState.pangeaController,
|
||||
);
|
||||
break;
|
||||
case SettingsAction.findAConversationPartner:
|
||||
findConversationPartnerDialog(
|
||||
context,
|
||||
MatrixState.pangeaController,
|
||||
);
|
||||
break;
|
||||
// case SettingsAction.spaceAnalytics:
|
||||
// context.go('/rooms/analytics');
|
||||
// break;
|
||||
// case SettingsAction.myAnalytics:
|
||||
// context.go('/rooms/mylearning');
|
||||
// break;
|
||||
case SettingsAction.logout:
|
||||
pLogoutAction(context);
|
||||
break;
|
||||
|
|
@ -471,17 +418,15 @@ enum SettingsAction {
|
|||
addAccount,
|
||||
newGroup,
|
||||
newSpace,
|
||||
setStatus,
|
||||
invite,
|
||||
settings,
|
||||
archive,
|
||||
// #Pangea
|
||||
learning,
|
||||
// setStatus,
|
||||
// invite,
|
||||
// Pangea#
|
||||
settings,
|
||||
// #Pangea
|
||||
// archive,
|
||||
joinWithClassCode,
|
||||
// spaceAnalytics,
|
||||
// myAnalytics,
|
||||
findAConversationPartner,
|
||||
learning,
|
||||
logout,
|
||||
newClass,
|
||||
// Pangea#
|
||||
}
|
||||
|
|
|
|||
|
|
@ -315,7 +315,10 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
actions: [
|
||||
AlertDialogAction(
|
||||
key: AddRoomType.subspace,
|
||||
label: L10n.of(context)!.createNewSpace,
|
||||
// #Pangea
|
||||
// label: L10n.of(context)!.createNewSpace,
|
||||
label: L10n.of(context)!.newChat,
|
||||
// Pangea#
|
||||
),
|
||||
AlertDialogAction(
|
||||
key: AddRoomType.chat,
|
||||
|
|
|
|||
|
|
@ -1,33 +1,14 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fluffychat/pages/new_group/new_group_view.dart';
|
||||
import 'package:fluffychat/pangea/constants/bot_mode.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/chat_topic_model.dart';
|
||||
import 'package:fluffychat/pangea/models/lemma.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/utils/class_chat_power_levels.dart';
|
||||
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
|
||||
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
|
||||
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings.dart';
|
||||
import 'package:fluffychat/utils/file_selector.dart';
|
||||
import 'package:fluffychat/widgets/matrix.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' as sdk;
|
||||
|
||||
class NewGroup extends StatefulWidget {
|
||||
// #Pangea
|
||||
final String? spaceId;
|
||||
|
||||
const NewGroup({
|
||||
super.key,
|
||||
this.spaceId,
|
||||
});
|
||||
// Pangea#
|
||||
const NewGroup({super.key});
|
||||
|
||||
@override
|
||||
NewGroupController createState() => NewGroupController();
|
||||
|
|
@ -47,50 +28,25 @@ class NewGroupController extends State<NewGroup> {
|
|||
|
||||
bool loading = false;
|
||||
|
||||
// #Pangea
|
||||
PangeaController pangeaController = MatrixState.pangeaController;
|
||||
final GlobalKey<AddToSpaceState> addToSpaceKey = GlobalKey<AddToSpaceState>();
|
||||
final GlobalKey<ConversationBotSettingsState> addConversationBotKey =
|
||||
GlobalKey<ConversationBotSettingsState>();
|
||||
final GlobalKey<RoomCapacityButtonState> addCapacityKey =
|
||||
GlobalKey<RoomCapacityButtonState>();
|
||||
|
||||
ChatTopic chatTopic = ChatTopic.empty;
|
||||
|
||||
void setVocab(List<Lemma> vocab) => setState(() => chatTopic.vocab = vocab);
|
||||
|
||||
String? get activeSpaceId =>
|
||||
GoRouterState.of(context).uri.queryParameters['spaceId'];
|
||||
// Pangea#
|
||||
|
||||
void setPublicGroup(bool b) => setState(() => publicGroup = b);
|
||||
|
||||
void setGroupCanBeFound(bool b) => setState(() => groupCanBeFound = b);
|
||||
|
||||
void selectPhoto() async {
|
||||
final photo = await FilePicker.platform.pickFiles(
|
||||
type: FileType.image,
|
||||
final photo = await selectFiles(
|
||||
context,
|
||||
type: FileSelectorType.images,
|
||||
allowMultiple: false,
|
||||
withData: true,
|
||||
);
|
||||
final bytes = await photo.singleOrNull?.readAsBytes();
|
||||
|
||||
setState(() {
|
||||
avatarUrl = null;
|
||||
avatar = photo?.files.singleOrNull?.bytes;
|
||||
avatar = bytes;
|
||||
});
|
||||
}
|
||||
|
||||
void submitAction([_]) async {
|
||||
// #Pangea
|
||||
if (nameController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.emptyChatNameWarning),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Pangea#
|
||||
final client = Matrix.of(context).client;
|
||||
|
||||
try {
|
||||
|
|
@ -104,53 +60,12 @@ class NewGroupController extends State<NewGroup> {
|
|||
|
||||
if (!mounted) return;
|
||||
|
||||
// #Pangea
|
||||
// validate init bot options
|
||||
final addBot = addConversationBotKey.currentState?.addBot ?? false;
|
||||
if (addBot) {
|
||||
final botOptions = addConversationBotKey.currentState!.botOptions;
|
||||
if (botOptions.mode == BotMode.custom) {
|
||||
if (botOptions.customSystemPrompt == null ||
|
||||
botOptions.customSystemPrompt!.isEmpty) {
|
||||
setState(() {
|
||||
error = L10n.of(context)!
|
||||
.conversationBotCustomZone_customSystemPromptEmptyError;
|
||||
loading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if (botOptions.mode == BotMode.textAdventure) {
|
||||
if (botOptions.textAdventureGameMasterInstructions == null ||
|
||||
botOptions.textAdventureGameMasterInstructions!.isEmpty) {
|
||||
setState(() {
|
||||
error = L10n.of(context)!
|
||||
.conversationBotCustomZone_instructionSystemPromptEmptyError;
|
||||
loading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
final roomId = await client.createGroupChat(
|
||||
// #Pangea
|
||||
// visibility:
|
||||
// publicGroup ? sdk.Visibility.public : sdk.Visibility.private,
|
||||
// preset: publicGroup
|
||||
// ? sdk.CreateRoomPreset.publicChat
|
||||
// : sdk.CreateRoomPreset.privateChat,
|
||||
preset: sdk.CreateRoomPreset.publicChat,
|
||||
powerLevelContentOverride:
|
||||
await ClassChatPowerLevels.powerLevelOverrideForClassChat(
|
||||
context,
|
||||
addToSpaceKey.currentState!.parent,
|
||||
),
|
||||
invite: [
|
||||
if (addConversationBotKey.currentState?.addBot ?? false)
|
||||
BotName.byEnvironment,
|
||||
],
|
||||
// Pangea#
|
||||
visibility:
|
||||
groupCanBeFound ? sdk.Visibility.public : sdk.Visibility.private,
|
||||
preset: publicGroup
|
||||
? sdk.CreateRoomPreset.publicChat
|
||||
: sdk.CreateRoomPreset.privateChat,
|
||||
groupName: nameController.text.isNotEmpty ? nameController.text : null,
|
||||
initialState: [
|
||||
if (avatar != null)
|
||||
|
|
@ -158,29 +73,9 @@ class NewGroupController extends State<NewGroup> {
|
|||
type: sdk.EventTypes.RoomAvatar,
|
||||
content: {'url': avatarUrl.toString()},
|
||||
),
|
||||
// #Pangea
|
||||
if (addConversationBotKey.currentState?.addBot ?? false)
|
||||
addConversationBotKey.currentState!.botOptions.toStateEvent,
|
||||
// Pangea#
|
||||
],
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (publicGroup && groupCanBeFound) {
|
||||
await client.setRoomVisibilityOnDirectory(
|
||||
roomId,
|
||||
visibility: sdk.Visibility.public,
|
||||
);
|
||||
}
|
||||
// #Pangea
|
||||
GoogleAnalytics.createChat(roomId);
|
||||
await addToSpaceKey.currentState!.addSpaces(roomId);
|
||||
|
||||
final capacity = addCapacityKey.currentState?.capacity;
|
||||
final room = client.getRoomById(roomId);
|
||||
if (capacity != null && room != null) {
|
||||
room.updateRoomCapacity(capacity);
|
||||
}
|
||||
// Pangea#
|
||||
context.go('/rooms/$roomId/invite');
|
||||
} catch (e, s) {
|
||||
sdk.Logs().d('Unable to create group', e, s);
|
||||
|
|
@ -191,20 +86,6 @@ class NewGroupController extends State<NewGroup> {
|
|||
}
|
||||
}
|
||||
|
||||
//#Pangea
|
||||
@override
|
||||
void initState() {
|
||||
Future.delayed(Duration.zero, () {
|
||||
chatTopic.langCode =
|
||||
pangeaController.languageController.userL2?.langCode ??
|
||||
pangeaController.pLanguageStore.targetOptions.first.langCode;
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
super.initState();
|
||||
}
|
||||
//Pangea#
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => NewGroupView(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/new_group/new_group.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart';
|
||||
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
|
||||
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
|
|
@ -27,20 +24,8 @@ class NewGroupView extends StatelessWidget {
|
|||
onPressed: controller.loading ? null : Navigator.of(context).pop,
|
||||
),
|
||||
),
|
||||
// #Pangea
|
||||
// title: Text(L10n.of(context)!.createGroup),
|
||||
title: Text(L10n.of(context)!.createChat),
|
||||
// Pangea#
|
||||
title: Text(L10n.of(context)!.createGroup),
|
||||
),
|
||||
// #Pangea
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: controller.loading ? null : controller.submitAction,
|
||||
icon: controller.loading ? null : const Icon(Icons.chat_bubble_outline),
|
||||
label: controller.loading
|
||||
? const CircularProgressIndicator.adaptive()
|
||||
: Text(L10n.of(context)!.createChat),
|
||||
),
|
||||
// Pangea#
|
||||
body: MaxWidthBody(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
|
@ -68,9 +53,6 @@ class NewGroupView extends StatelessWidget {
|
|||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: TextField(
|
||||
// #Pangea
|
||||
maxLength: 64,
|
||||
// Pangea#
|
||||
autofocus: true,
|
||||
controller: controller.nameController,
|
||||
autocorrect: false,
|
||||
|
|
@ -85,40 +67,31 @@ class NewGroupView extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile.adaptive(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
secondary: const Icon(Icons.public_outlined),
|
||||
title: Text(L10n.of(context)!.groupIsPublic),
|
||||
value: controller.publicGroup,
|
||||
onChanged: controller.loading ? null : controller.setPublicGroup,
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: controller.publicGroup
|
||||
? SwitchListTile.adaptive(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 32),
|
||||
secondary: const Icon(Icons.search_outlined),
|
||||
title: Text(L10n.of(context)!.groupCanBeFoundViaSearch),
|
||||
value: controller.groupCanBeFound,
|
||||
onChanged: controller.loading
|
||||
? null
|
||||
: controller.setGroupCanBeFound,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
// #Pangea
|
||||
RoomCapacityButton(
|
||||
key: controller.addCapacityKey,
|
||||
),
|
||||
ConversationBotSettings(
|
||||
key: controller.addConversationBotKey,
|
||||
activeSpaceId: controller.activeSpaceId,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
AddToSpaceToggles(
|
||||
key: controller.addToSpaceKey,
|
||||
startOpen: true,
|
||||
activeSpaceId: controller.activeSpaceId,
|
||||
),
|
||||
// SwitchListTile.adaptive(
|
||||
// secondary: const Icon(Icons.public_outlined),
|
||||
// title: Text(L10n.of(context)!.groupIsPublic),
|
||||
// value: controller.publicGroup,
|
||||
// onChanged: controller.loading ? null : controller.setPublicGroup,
|
||||
// ),
|
||||
// AnimatedSize(
|
||||
// duration: FluffyThemes.animationDuration,
|
||||
// child: controller.publicGroup
|
||||
// ? SwitchListTile.adaptive(
|
||||
// secondary: const Icon(Icons.search_outlined),
|
||||
// title: Text(L10n.of(context)!.groupCanBeFoundViaSearch),
|
||||
// value: controller.groupCanBeFound,
|
||||
// onChanged: controller.loading
|
||||
// ? null
|
||||
// : controller.setGroupCanBeFound,
|
||||
// )
|
||||
// : const SizedBox.shrink(),
|
||||
// ),
|
||||
// SwitchListTile.adaptive(
|
||||
// contentPadding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
// secondary: Icon(
|
||||
// Icons.lock_outlined,
|
||||
// color: theme.colorScheme.onSurface,
|
||||
|
|
@ -132,29 +105,20 @@ class NewGroupView extends StatelessWidget {
|
|||
// value: !controller.publicGroup,
|
||||
// onChanged: null,
|
||||
// ),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.all(16.0),
|
||||
// child: SizedBox(
|
||||
// width: double.infinity,
|
||||
// child: ElevatedButton(
|
||||
// onPressed:
|
||||
// controller.loading ? null : controller.submitAction,
|
||||
// child: controller.loading
|
||||
// ? const LinearProgressIndicator()
|
||||
// : Row(
|
||||
// children: [
|
||||
// Expanded(
|
||||
// child: Text(
|
||||
// L10n.of(context)!.createGroupAndInviteUsers,
|
||||
// ),
|
||||
// ),
|
||||
// Icon(Icons.adaptive.arrow_forward_outlined),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Pangea#
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed:
|
||||
controller.loading ? null : controller.submitAction,
|
||||
child: controller.loading
|
||||
? const LinearProgressIndicator()
|
||||
: Text(L10n.of(context)!.createGroupAndInviteUsers),
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: error == null
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import 'package:file_picker/file_picker.dart';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:fluffychat/pages/new_space/new_space_view.dart';
|
||||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/utils/class_chat_power_levels.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
|
||||
import 'package:fluffychat/pangea/utils/space_code.dart';
|
||||
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
|
||||
import 'package:fluffychat/utils/file_selector.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart' as sdk;
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
|
|
@ -26,48 +27,32 @@ class NewSpace extends StatefulWidget {
|
|||
class NewSpaceController extends State<NewSpace> {
|
||||
TextEditingController nameController = TextEditingController();
|
||||
TextEditingController topicController = TextEditingController();
|
||||
// #Pangea
|
||||
// bool publicGroup = false;
|
||||
bool publicGroup = true;
|
||||
// final GlobalKey<RoomRulesState> rulesEditorKey = GlobalKey<RoomRulesState>();
|
||||
final GlobalKey<AddToSpaceState> addToSpaceKey = GlobalKey<AddToSpaceState>();
|
||||
// commenting out language settings in spaces for now
|
||||
// final GlobalKey<LanguageSettingsState> languageSettingsKey =
|
||||
// GlobalKey<LanguageSettingsState>();
|
||||
final GlobalKey<RoomCapacityButtonState> addCapacityKey =
|
||||
GlobalKey<RoomCapacityButtonState>();
|
||||
|
||||
//Pangea#
|
||||
bool publicGroup = false;
|
||||
bool loading = false;
|
||||
// #Pangea
|
||||
// String? nameError;
|
||||
// String? topicError;
|
||||
// Pangea#
|
||||
String? nameError;
|
||||
String? topicError;
|
||||
|
||||
Uint8List? avatar;
|
||||
|
||||
Uri? avatarUrl;
|
||||
|
||||
void selectPhoto() async {
|
||||
final photo = await FilePicker.platform.pickFiles(
|
||||
type: FileType.image,
|
||||
allowMultiple: false,
|
||||
withData: true,
|
||||
final photo = await selectFiles(
|
||||
context,
|
||||
type: FileSelectorType.images,
|
||||
);
|
||||
|
||||
final bytes = await photo.firstOrNull?.readAsBytes();
|
||||
setState(() {
|
||||
avatarUrl = null;
|
||||
avatar = photo?.files.singleOrNull?.bytes;
|
||||
avatar = bytes;
|
||||
});
|
||||
}
|
||||
|
||||
void setPublicGroup(bool b) => setState(() => publicGroup = b);
|
||||
|
||||
// #Pangea
|
||||
List<StateEvent> get initialState {
|
||||
final events = <StateEvent>[];
|
||||
|
||||
events.add(
|
||||
List<StateEvent> initialState(String joinCode) {
|
||||
return [
|
||||
StateEvent(
|
||||
type: EventTypes.RoomPowerLevels,
|
||||
stateKey: '',
|
||||
|
|
@ -82,65 +67,26 @@ class NewSpaceController extends State<NewSpace> {
|
|||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// commenting out pangea room rules in spaces for now
|
||||
// if (rulesEditorKey.currentState?.rules != null) {
|
||||
// events.add(rulesEditorKey.currentState!.rules.toStateEvent);
|
||||
// } else {
|
||||
// debugger(when: kDebugMode);
|
||||
// }
|
||||
// commenting out language settings in spaces for now
|
||||
// if (languageSettingsKey.currentState != null) {
|
||||
// events
|
||||
// .add(languageSettingsKey.currentState!.languageSettings.toStateEvent);
|
||||
// }
|
||||
|
||||
return events;
|
||||
StateEvent(
|
||||
type: sdk.EventTypes.RoomJoinRules,
|
||||
content: {
|
||||
ModelKey.joinRule:
|
||||
sdk.JoinRules.knock.toString().replaceAll('JoinRules.', ''),
|
||||
ModelKey.accessCode: joinCode,
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
//Pangea#
|
||||
|
||||
void submitAction([_]) async {
|
||||
final client = Matrix.of(context).client;
|
||||
setState(() {
|
||||
// #Pangea
|
||||
// nameError = topicError = null;
|
||||
// Pangea#
|
||||
nameError = topicError = null;
|
||||
});
|
||||
// #Pangea
|
||||
// commenting out pangea room rules in spaces for now
|
||||
// if (rulesEditorKey.currentState == null) {
|
||||
// debugger(when: kDebugMode);
|
||||
// return;
|
||||
// }
|
||||
// commenting out language settings in spaces for now
|
||||
// if (languageSettingsKey.currentState != null &&
|
||||
// languageSettingsKey.currentState!.sameLanguages) {
|
||||
// ScaffoldMessenger.of(context).showSnackBar(
|
||||
// SnackBar(
|
||||
// content: Text(L10n.of(context)!.noIdenticalLanguages),
|
||||
// ),
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
// final int? languageLevel =
|
||||
// languageSettingsKey.currentState!.languageSettings.languageLevel;
|
||||
// if (languageLevel == null) {
|
||||
// ScaffoldMessenger.of(context).showSnackBar(
|
||||
// SnackBar(content: Text(L10n.of(context)!.languageLevelWarning)),
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
// Pangea#
|
||||
if (nameController.text.isEmpty) {
|
||||
setState(() {
|
||||
// #Pangea
|
||||
// nameError = L10n.of(context)!.pleaseChoose;
|
||||
final String warning = L10n.of(context)!.emptySpaceNameWarning;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(warning)),
|
||||
);
|
||||
// Pangea#
|
||||
nameError = L10n.of(context)!.pleaseChoose;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -150,69 +96,41 @@ class NewSpaceController extends State<NewSpace> {
|
|||
try {
|
||||
final avatar = this.avatar;
|
||||
avatarUrl ??= avatar == null ? null : await client.uploadContent(avatar);
|
||||
// #Pangea
|
||||
final joinCode = await SpaceCodeUtil.generateSpaceCode(client);
|
||||
// Pangea#
|
||||
|
||||
final spaceId = await client.createRoom(
|
||||
// #Pangea
|
||||
// preset: publicGroup
|
||||
// ? sdk.CreateRoomPreset.publicChat
|
||||
// : sdk.CreateRoomPreset.privateChat,
|
||||
preset: sdk.CreateRoomPreset.publicChat,
|
||||
// Pangea#
|
||||
preset: publicGroup
|
||||
? sdk.CreateRoomPreset.publicChat
|
||||
: sdk.CreateRoomPreset.privateChat,
|
||||
creationContent: {'type': RoomCreationTypes.mSpace},
|
||||
visibility: publicGroup ? sdk.Visibility.public : null,
|
||||
// #Pangea
|
||||
// roomAliasName: publicGroup
|
||||
// ? nameController.text.trim().toLowerCase().replaceAll(' ', '_')
|
||||
// : null,
|
||||
roomAliasName: SpaceCodeUtil.generateSpaceCode(),
|
||||
// Pangea#
|
||||
roomAliasName: publicGroup
|
||||
? nameController.text.trim().toLowerCase().replaceAll(' ', '_')
|
||||
: null,
|
||||
name: nameController.text.trim(),
|
||||
topic: topicController.text.isEmpty ? null : topicController.text,
|
||||
// #Pangea
|
||||
// powerLevelContentOverride: {'events_default': 100},
|
||||
powerLevelContentOverride: addToSpaceKey.currentState != null
|
||||
? await ClassChatPowerLevels.powerLevelOverrideForClassChat(
|
||||
context,
|
||||
addToSpaceKey.currentState!.parent,
|
||||
)
|
||||
: null,
|
||||
// Pangea#
|
||||
powerLevelContentOverride: {'events_default': 100},
|
||||
initialState: [
|
||||
// #Pangea
|
||||
...initialState(joinCode),
|
||||
// Pangea#
|
||||
if (avatar != null)
|
||||
sdk.StateEvent(
|
||||
type: sdk.EventTypes.RoomAvatar,
|
||||
content: {'url': avatarUrl.toString()},
|
||||
),
|
||||
// #Pangea
|
||||
...initialState,
|
||||
// Pangea#
|
||||
],
|
||||
);
|
||||
if (!mounted) return;
|
||||
// #Pangea
|
||||
final List<Future<dynamic>> futures = [
|
||||
Matrix.of(context).client.waitForRoomInSync(spaceId, join: true),
|
||||
];
|
||||
if (addToSpaceKey.currentState != null) {
|
||||
futures.add(addToSpaceKey.currentState!.addSpaces(spaceId));
|
||||
}
|
||||
await Future.wait(futures);
|
||||
|
||||
final capacity = addCapacityKey.currentState?.capacity;
|
||||
final space = client.getRoomById(spaceId);
|
||||
if (capacity != null && space != null) {
|
||||
space.updateRoomCapacity(capacity);
|
||||
}
|
||||
|
||||
final Room? room = Matrix.of(context).client.getRoomById(spaceId);
|
||||
Room? room = client.getRoomById(spaceId);
|
||||
if (room == null) {
|
||||
ErrorHandler.logError(
|
||||
e: 'Failed to get new space by id $spaceId',
|
||||
);
|
||||
MatrixState.pangeaController.classController
|
||||
.setActiveSpaceIdInChatListController(spaceId);
|
||||
return;
|
||||
await Matrix.of(context).client.waitForRoomInSync(spaceId);
|
||||
room = client.getRoomById(spaceId);
|
||||
}
|
||||
|
||||
if (room == null) return;
|
||||
GoogleAnalytics.createClass(room.name, room.classCode);
|
||||
try {
|
||||
await room.invite(BotName.byEnvironment);
|
||||
|
|
@ -221,18 +139,13 @@ class NewSpaceController extends State<NewSpace> {
|
|||
e: "Failed to invite pangea bot to space ${room.id}",
|
||||
);
|
||||
}
|
||||
// Pangea#
|
||||
if (!mounted) return;
|
||||
// #Pangea
|
||||
// context.pop<String>(spaceId);
|
||||
MatrixState.pangeaController.classController
|
||||
.setActiveSpaceIdInChatListController(spaceId);
|
||||
// Pangea#
|
||||
context.pop<String>(spaceId);
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
// #Pangea
|
||||
// topicError = e.toLocalizedString(context);
|
||||
// Pangea#
|
||||
topicError = e.toLocalizedString(context);
|
||||
});
|
||||
} finally {
|
||||
setState(() {
|
||||
|
|
@ -243,10 +156,5 @@ class NewSpaceController extends State<NewSpace> {
|
|||
}
|
||||
|
||||
@override
|
||||
// #Pangea
|
||||
// Widget build(BuildContext context) => NewSpaceView(this);
|
||||
Widget build(BuildContext context) {
|
||||
return NewSpaceView(this);
|
||||
}
|
||||
// Pangea#
|
||||
Widget build(BuildContext context) => NewSpaceView(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart';
|
||||
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -19,15 +17,6 @@ class NewSpaceView extends StatelessWidget {
|
|||
appBar: AppBar(
|
||||
title: Text(L10n.of(context)!.createNewSpace),
|
||||
),
|
||||
// #Pangea
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: controller.loading ? null : controller.submitAction,
|
||||
icon: controller.loading ? null : const Icon(Icons.workspaces_outlined),
|
||||
label: controller.loading
|
||||
? const CircularProgressIndicator.adaptive()
|
||||
: Text(L10n.of(context)!.createSpace),
|
||||
),
|
||||
// Pangea#
|
||||
body: MaxWidthBody(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
|
@ -62,58 +51,38 @@ class NewSpaceView extends StatelessWidget {
|
|||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.people_outlined),
|
||||
labelText: L10n.of(context)!.spaceName,
|
||||
// #Pangea
|
||||
// errorText: controller.nameError,
|
||||
// Pangea#
|
||||
errorText: controller.nameError,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// #Pangea
|
||||
RoomCapacityButton(
|
||||
key: controller.addCapacityKey,
|
||||
spaceMode: true,
|
||||
SwitchListTile.adaptive(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
title: Text(L10n.of(context)!.spaceIsPublic),
|
||||
value: controller.publicGroup,
|
||||
onChanged: controller.setPublicGroup,
|
||||
),
|
||||
AddToSpaceToggles(
|
||||
key: controller.addToSpaceKey,
|
||||
startOpen: true,
|
||||
spaceMode: true,
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
trailing: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Icon(Icons.info_outlined),
|
||||
),
|
||||
subtitle: Text(L10n.of(context)!.newSpaceDescription),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed:
|
||||
controller.loading ? null : controller.submitAction,
|
||||
child: controller.loading
|
||||
? const LinearProgressIndicator()
|
||||
: Text(L10n.of(context)!.createNewSpace),
|
||||
),
|
||||
),
|
||||
),
|
||||
// SwitchListTile.adaptive(
|
||||
// title: Text(L10n.of(context)!.spaceIsPublic),
|
||||
// value: controller.publicGroup,
|
||||
// onChanged: controller.setPublicGroup,
|
||||
// ),
|
||||
// ListTile(
|
||||
// trailing: const Padding(
|
||||
// padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
// child: Icon(Icons.info_outlined),
|
||||
// ),
|
||||
// subtitle: Text(L10n.of(context)!.newSpaceDescription),
|
||||
// ),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.all(16.0),
|
||||
// child: SizedBox(
|
||||
// width: double.infinity,
|
||||
// child: ElevatedButton(
|
||||
// onPressed:
|
||||
// controller.loading ? null : controller.submitAction,
|
||||
// child: controller.loading
|
||||
// ? const LinearProgressIndicator()
|
||||
// : Row(
|
||||
// children: [
|
||||
// Expanded(
|
||||
// child: Text(
|
||||
// L10n.of(context)!.createNewSpace,
|
||||
// ),
|
||||
// ),
|
||||
// Icon(Icons.adaptive.arrow_forward_outlined),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Pangea#
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:fluffychat/utils/fluffy_share.dart';
|
|||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
|
@ -18,14 +19,6 @@ class SettingsView extends StatelessWidget {
|
|||
|
||||
const SettingsView(this.controller, {super.key});
|
||||
|
||||
// #Pangea
|
||||
Future<String> getAppVersion(BuildContext context) async {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
return L10n.of(context)!
|
||||
.versionText(packageInfo.version, packageInfo.buildNumber);
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
|
@ -218,14 +211,34 @@ class SettingsView extends StatelessWidget {
|
|||
onTap: () => launchUrlString(AppConfig.termsOfServiceUrl),
|
||||
trailing: const Icon(Icons.open_in_new_outlined),
|
||||
),
|
||||
FutureBuilder<String>(
|
||||
future: getAppVersion(context),
|
||||
FutureBuilder<PackageInfo>(
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
trailing: const Icon(Icons.copy_outlined),
|
||||
onTap: () async {
|
||||
if (snapshot.data == null) return;
|
||||
await Clipboard.setData(
|
||||
ClipboardData(
|
||||
text:
|
||||
"${snapshot.data!.version}+${snapshot.data!.buildNumber}",
|
||||
),
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.copiedToClipboard),
|
||||
),
|
||||
);
|
||||
},
|
||||
title: Text(
|
||||
snapshot.data ?? L10n.of(context)!.versionNotFound,
|
||||
snapshot.data != null
|
||||
? L10n.of(context)!.versionText(
|
||||
snapshot.data!.version,
|
||||
snapshot.data!.buildNumber,
|
||||
)
|
||||
: L10n.of(context)!.versionNotFound,
|
||||
),
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
class BotMode {
|
||||
static const direct = "direct";
|
||||
static const directChat = "direct_chat";
|
||||
static const discussion = "discussion";
|
||||
static const custom = "custom";
|
||||
static const storyGame = "story_game";
|
||||
|
|
|
|||
1
lib/pangea/constants/class_code_constants.dart
Normal file
1
lib/pangea/constants/class_code_constants.dart
Normal file
|
|
@ -0,0 +1 @@
|
|||
const String noClassCode = 'No class code!';
|
||||
|
|
@ -6,4 +6,5 @@ class PLocalKey {
|
|||
static const String paywallBackoff = 'paywallBackoff';
|
||||
static const String messagesSinceUpdate = 'messagesSinceLastUpdate';
|
||||
static const String completedActivities = 'completedActivities';
|
||||
static const String justInputtedCode = 'justInputtedCode';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,6 +121,13 @@ class ModelKey {
|
|||
static const String textAdventureGameMasterInstructions =
|
||||
"text_adventure_game_master_instructions";
|
||||
|
||||
static const String targetLanguage = "target_language";
|
||||
static const String targetVoice = "target_voice";
|
||||
|
||||
static const String prevEventId = "prev_event_id";
|
||||
static const String prevLastUpdated = "prev_last_updated";
|
||||
|
||||
// room code
|
||||
static const String joinRule = "join_rule";
|
||||
static const String accessCode = "access_code";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
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';
|
||||
|
|
@ -9,15 +9,14 @@ import 'package:fluffychat/pangea/extensions/client_extension/client_extension.d
|
|||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/utils/space_code.dart';
|
||||
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../widgets/matrix.dart';
|
||||
import '../utils/firebase_analytics.dart';
|
||||
import 'base_controller.dart';
|
||||
|
||||
class ClassController extends BaseController {
|
||||
|
|
@ -50,97 +49,134 @@ class ClassController extends BaseController {
|
|||
);
|
||||
|
||||
if (classCode != null) {
|
||||
await _pangeaController.pStoreService.delete(
|
||||
PLocalKey.cachedClassCodeToJoin,
|
||||
isAccountData: false,
|
||||
);
|
||||
await joinClasswithCode(
|
||||
context,
|
||||
classCode,
|
||||
).onError(
|
||||
(error, stackTrace) =>
|
||||
SpaceCodeUtil.messageSnack(context, ErrorCopy(context, error).body),
|
||||
);
|
||||
await _pangeaController.pStoreService.delete(
|
||||
PLocalKey.cachedClassCodeToJoin,
|
||||
isAccountData: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> joinClasswithCode(BuildContext context, String classCode) async {
|
||||
try {
|
||||
final QueryPublicRoomsResponse queryPublicRoomsResponse =
|
||||
await Matrix.of(context).client.queryPublicRooms(
|
||||
limit: 1,
|
||||
filter: PublicRoomQueryFilter(genericSearchTerm: classCode),
|
||||
);
|
||||
|
||||
final PublicRoomsChunk? classChunk =
|
||||
queryPublicRoomsResponse.chunk.firstWhereOrNull((element) {
|
||||
return element.canonicalAlias?.replaceAll("#", "").split(":")[0] ==
|
||||
classCode;
|
||||
});
|
||||
|
||||
if (classChunk == null) {
|
||||
SpaceCodeUtil.messageSnack(
|
||||
context,
|
||||
L10n.of(context)!.unableToFindClass,
|
||||
final client = Matrix.of(context).client;
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
final knockResponse = await client.httpClient.post(
|
||||
Uri.parse(
|
||||
'${client.homeserver}/_synapse/client/pangea/v1/knock_with_code',
|
||||
),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ${client.accessToken}',
|
||||
},
|
||||
body: jsonEncode({'access_code': classCode}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (knockResponse.statusCode == 429) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
throw L10n.of(context)!.tooManyRequest;
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (knockResponse.statusCode != 200) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
throw L10n.of(context)!.unableToFindClass;
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
final knockResult = jsonDecode(knockResponse.body);
|
||||
final foundClasses = List<String>.from(knockResult['rooms']);
|
||||
final alreadyJoined = List<String>.from(knockResult['already_joined']);
|
||||
if (alreadyJoined.isNotEmpty) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
throw L10n.of(context)!.alreadyInClass;
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (foundClasses.isEmpty) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
throw L10n.of(context)!.unableToFindClass;
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
final chosenClassId = foundClasses.first;
|
||||
if (_pangeaController.matrixState.client.rooms
|
||||
.any((room) => room.id == chosenClassId)) {
|
||||
setActiveSpaceIdInChatListController(chosenClassId);
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
throw L10n.of(context)!.alreadyInClass;
|
||||
},
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
await _pangeaController.pStoreService.save(
|
||||
PLocalKey.justInputtedCode,
|
||||
classCode,
|
||||
isAccountData: false,
|
||||
);
|
||||
await client.joinRoomById(chosenClassId);
|
||||
_pangeaController.pStoreService.delete(PLocalKey.justInputtedCode);
|
||||
}
|
||||
|
||||
if (_pangeaController.matrixState.client.rooms
|
||||
.any((room) => room.id == classChunk.roomId)) {
|
||||
setActiveSpaceIdInChatListController(classChunk.roomId);
|
||||
SpaceCodeUtil.messageSnack(context, L10n.of(context)!.alreadyInClass);
|
||||
return;
|
||||
}
|
||||
if (_pangeaController.matrixState.client.getRoomById(chosenClassId) ==
|
||||
null) {
|
||||
await _pangeaController.matrixState.client.waitForRoomInSync(
|
||||
chosenClassId,
|
||||
join: true,
|
||||
);
|
||||
}
|
||||
|
||||
await _pangeaController.matrixState.client.joinRoom(classChunk.roomId);
|
||||
|
||||
if (_pangeaController.matrixState.client.getRoomById(classChunk.roomId) ==
|
||||
null) {
|
||||
await _pangeaController.matrixState.client.waitForRoomInSync(
|
||||
classChunk.roomId,
|
||||
join: true,
|
||||
// If the room is full, leave
|
||||
final room =
|
||||
_pangeaController.matrixState.client.getRoomById(chosenClassId);
|
||||
if (room == null) {
|
||||
return;
|
||||
}
|
||||
final joinResult = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
if (await room.leaveIfFull()) {
|
||||
throw L10n.of(context)!.roomFull;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
if (joinResult.error != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the room is full, leave
|
||||
final room =
|
||||
_pangeaController.matrixState.client.getRoomById(classChunk.roomId);
|
||||
if (room == null) {
|
||||
setActiveSpaceIdInChatListController(chosenClassId);
|
||||
|
||||
// add the user's analytics room to this joined space
|
||||
// so their teachers can join them via the space hierarchy
|
||||
final Room? joinedSpace =
|
||||
_pangeaController.matrixState.client.getRoomById(chosenClassId);
|
||||
|
||||
// when possible, add user's analytics room the to space they joined
|
||||
joinedSpace?.addAnalyticsRoomsToSpace();
|
||||
|
||||
// and invite the space's teachers to the user's analytics rooms
|
||||
joinedSpace?.inviteSpaceTeachersToAnalyticsRooms();
|
||||
GoogleAnalytics.joinClass(classCode);
|
||||
return;
|
||||
}
|
||||
final joinResult = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
if (await room.leaveIfFull()) {
|
||||
throw L10n.of(context)!.roomFull;
|
||||
}
|
||||
},
|
||||
);
|
||||
if (joinResult.error != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveSpaceIdInChatListController(classChunk.roomId);
|
||||
|
||||
// add the user's analytics room to this joined space
|
||||
// so their teachers can join them via the space hierarchy
|
||||
final Room? joinedSpace =
|
||||
_pangeaController.matrixState.client.getRoomById(classChunk.roomId);
|
||||
|
||||
// when possible, add user's analytics room the to space they joined
|
||||
joinedSpace?.addAnalyticsRoomsToSpace();
|
||||
|
||||
// and invite the space's teachers to the user's analytics rooms
|
||||
joinedSpace?.inviteSpaceTeachersToAnalyticsRooms();
|
||||
GoogleAnalytics.joinClass(classCode);
|
||||
return;
|
||||
} catch (err) {
|
||||
SpaceCodeUtil.messageSnack(
|
||||
context,
|
||||
ErrorCopy(context, err).body,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
// P-EPIC
|
||||
// prereq - server needs ability to invite to private room. how?
|
||||
// does server api have ability with admin token?
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ class PangeaController {
|
|||
return;
|
||||
}
|
||||
|
||||
const List<Room> botDMs = [];
|
||||
final List<Room> botDMs = [];
|
||||
for (final room in matrixState.client.rooms) {
|
||||
if (await room.isBotDM) {
|
||||
botDMs.add(room);
|
||||
|
|
|
|||
|
|
@ -5,13 +5,10 @@ import 'dart:developer';
|
|||
import 'package:fluffychat/pangea/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/network/requests.dart';
|
||||
import 'package:fluffychat/pangea/network/urls.dart';
|
||||
|
|
@ -22,11 +19,11 @@ import 'package:matrix/matrix.dart';
|
|||
/// Represents an item in the completion cache.
|
||||
class _RequestCacheItem {
|
||||
MessageActivityRequest req;
|
||||
PracticeActivityModel? practiceActivityEvent;
|
||||
PracticeActivityModel? practiceActivity;
|
||||
|
||||
_RequestCacheItem({
|
||||
required this.req,
|
||||
required this.practiceActivityEvent,
|
||||
required this.practiceActivity,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -109,64 +106,46 @@ class PracticeGenerationController {
|
|||
final int cacheKey = req.hashCode;
|
||||
|
||||
if (_cache.containsKey(cacheKey)) {
|
||||
return _cache[cacheKey]!.practiceActivityEvent;
|
||||
} else {
|
||||
//TODO - send request to server/bot, either via API or via event of type pangeaActivityReq
|
||||
// for now, just make and send the event from the client
|
||||
final MessageActivityResponse res = await _fetch(
|
||||
accessToken: _pangeaController.userController.accessToken,
|
||||
requestModel: req,
|
||||
);
|
||||
|
||||
if (res.finished) {
|
||||
debugPrint('Activity generation finished');
|
||||
return null;
|
||||
}
|
||||
|
||||
// if the server points to an existing event, return that event
|
||||
if (res.existingActivityEventId != null) {
|
||||
final Event? existingEvent =
|
||||
await event.room.getEventById(res.existingActivityEventId!);
|
||||
|
||||
debugPrint(
|
||||
'Existing activity event found: ${existingEvent?.content}',
|
||||
);
|
||||
if (existingEvent != null) {
|
||||
return PracticeActivityEvent(
|
||||
event: existingEvent,
|
||||
timeline: event.timeline,
|
||||
).practiceActivity;
|
||||
}
|
||||
}
|
||||
|
||||
if (res.activity == null) {
|
||||
debugPrint('No activity generated');
|
||||
return null;
|
||||
}
|
||||
|
||||
debugPrint('Activity generated: ${res.activity!.toJson()}');
|
||||
|
||||
_sendAndPackageEvent(res.activity!, event);
|
||||
_cache[cacheKey] =
|
||||
_RequestCacheItem(req: req, practiceActivityEvent: res.activity!);
|
||||
|
||||
return _cache[cacheKey]!.practiceActivityEvent;
|
||||
return _cache[cacheKey]!.practiceActivity;
|
||||
}
|
||||
}
|
||||
|
||||
PracticeActivityModel _dummyModel(PangeaMessageEvent event) =>
|
||||
PracticeActivityModel(
|
||||
tgtConstructs: [
|
||||
ConstructIdentifier(lemma: "be", type: ConstructTypeEnum.vocab),
|
||||
],
|
||||
activityType: ActivityTypeEnum.multipleChoice,
|
||||
langCode: event.messageDisplayLangCode,
|
||||
msgId: event.eventId,
|
||||
multipleChoice: MultipleChoice(
|
||||
question: "What is a synonym for 'happy'?",
|
||||
choices: ["sad", "angry", "joyful", "tired"],
|
||||
answer: "joyful",
|
||||
spanDisplayDetails: null,
|
||||
),
|
||||
final MessageActivityResponse res = await _fetch(
|
||||
accessToken: _pangeaController.userController.accessToken,
|
||||
requestModel: req,
|
||||
);
|
||||
|
||||
if (res.finished) {
|
||||
debugPrint('Activity generation finished');
|
||||
return null;
|
||||
}
|
||||
|
||||
// if the server points to an existing event, return that event
|
||||
if (res.existingActivityEventId != null) {
|
||||
final Event? existingEvent =
|
||||
await event.room.getEventById(res.existingActivityEventId!);
|
||||
|
||||
debugPrint(
|
||||
'Existing activity event found: ${existingEvent?.content}',
|
||||
);
|
||||
if (existingEvent != null) {
|
||||
return PracticeActivityEvent(
|
||||
event: existingEvent,
|
||||
timeline: event.timeline,
|
||||
).practiceActivity;
|
||||
}
|
||||
}
|
||||
|
||||
if (res.activity == null) {
|
||||
debugPrint('No activity generated');
|
||||
return null;
|
||||
}
|
||||
|
||||
debugPrint('Activity generated: ${res.activity!.toJson()}');
|
||||
|
||||
_sendAndPackageEvent(res.activity!, event);
|
||||
_cache[cacheKey] =
|
||||
_RequestCacheItem(req: req, practiceActivity: res.activity!);
|
||||
|
||||
return _cache[cacheKey]!.practiceActivity;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,20 +5,93 @@ import 'dart:typed_data';
|
|||
import 'package:fluffychat/pangea/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/network/urls.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import '../network/requests.dart';
|
||||
|
||||
class TextToSpeechRequest {
|
||||
String text;
|
||||
String langCode;
|
||||
class PangeaAudioEventData {
|
||||
final String text;
|
||||
final String langCode;
|
||||
final List<TTSToken> tokens;
|
||||
|
||||
TextToSpeechRequest({required this.text, required this.langCode});
|
||||
PangeaAudioEventData({
|
||||
required this.text,
|
||||
required this.langCode,
|
||||
required this.tokens,
|
||||
});
|
||||
|
||||
factory PangeaAudioEventData.fromJson(dynamic json) => PangeaAudioEventData(
|
||||
text: json[ModelKey.text] as String,
|
||||
langCode: json[ModelKey.langCode] as String,
|
||||
tokens: List<TTSToken>.from(
|
||||
(json[ModelKey.tokens] as Iterable)
|
||||
.map((x) => TTSToken.fromJson(x))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
ModelKey.text: text,
|
||||
ModelKey.langCode: langCode,
|
||||
ModelKey.tokens:
|
||||
List<Map<String, dynamic>>.from(tokens.map((x) => x.toJson())),
|
||||
};
|
||||
}
|
||||
|
||||
class TTSToken {
|
||||
final int startMS;
|
||||
final int endMS;
|
||||
final PangeaTokenText text;
|
||||
|
||||
TTSToken({
|
||||
required this.startMS,
|
||||
required this.endMS,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
factory TTSToken.fromJson(Map<String, dynamic> json) => TTSToken(
|
||||
startMS: json["start_ms"],
|
||||
endMS: json["end_ms"],
|
||||
text: PangeaTokenText.fromJson(json["text"]),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"start_ms": startMS,
|
||||
"end_ms": endMS,
|
||||
"text": text.toJson(),
|
||||
};
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is TTSToken &&
|
||||
other.startMS == startMS &&
|
||||
other.endMS == endMS &&
|
||||
other.text == text;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => startMS.hashCode ^ endMS.hashCode ^ text.hashCode;
|
||||
}
|
||||
|
||||
class TextToSpeechRequest {
|
||||
String text;
|
||||
String langCode;
|
||||
List<PangeaTokenText> tokens;
|
||||
|
||||
TextToSpeechRequest({
|
||||
required this.text,
|
||||
required this.langCode,
|
||||
required this.tokens,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
ModelKey.text: text,
|
||||
ModelKey.langCode: langCode,
|
||||
ModelKey.tokens: tokens.map((token) => token.toJson()).toList(),
|
||||
};
|
||||
|
||||
@override
|
||||
|
|
@ -40,6 +113,7 @@ class TextToSpeechResponse {
|
|||
int durationMillis;
|
||||
List<int> waveform;
|
||||
String fileExtension;
|
||||
List<TTSToken> ttsTokens;
|
||||
|
||||
TextToSpeechResponse({
|
||||
required this.audioContent,
|
||||
|
|
@ -47,6 +121,7 @@ class TextToSpeechResponse {
|
|||
required this.durationMillis,
|
||||
required this.waveform,
|
||||
required this.fileExtension,
|
||||
required this.ttsTokens,
|
||||
});
|
||||
|
||||
factory TextToSpeechResponse.fromJson(
|
||||
|
|
@ -58,7 +133,27 @@ class TextToSpeechResponse {
|
|||
durationMillis: json["duration_millis"],
|
||||
waveform: List<int>.from(json["wave_form"]),
|
||||
fileExtension: json["file_extension"],
|
||||
ttsTokens: List<TTSToken>.from(
|
||||
json["tts_tokens"].map((x) => TTSToken.fromJson(x)),
|
||||
),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"audio_content": audioContent,
|
||||
"mime_type": mimeType,
|
||||
"duration_millis": durationMillis,
|
||||
"wave_form": List<dynamic>.from(waveform.map((x) => x)),
|
||||
"file_extension": fileExtension,
|
||||
"tts_tokens": List<dynamic>.from(ttsTokens.map((x) => x.toJson())),
|
||||
};
|
||||
|
||||
PangeaAudioEventData toPangeaAudioEventData(String text, String langCode) {
|
||||
return PangeaAudioEventData(
|
||||
text: text,
|
||||
langCode: langCode,
|
||||
tokens: ttsTokens,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TextToSpeechCacheItem {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,6 @@
|
|||
enum ActivityDisplayInstructionsEnum { highlight, hide }
|
||||
enum ActivityDisplayInstructionsEnum { highlight, hide, nothing }
|
||||
|
||||
extension ActivityDisplayInstructionsEnumExt
|
||||
on ActivityDisplayInstructionsEnum {
|
||||
String get string {
|
||||
switch (this) {
|
||||
case ActivityDisplayInstructionsEnum.highlight:
|
||||
return 'highlight';
|
||||
case ActivityDisplayInstructionsEnum.hide:
|
||||
return 'hide';
|
||||
}
|
||||
}
|
||||
String get string => toString().split('.').last;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,12 @@
|
|||
enum ActivityTypeEnum { multipleChoice, freeResponse, listening, speaking }
|
||||
enum ActivityTypeEnum { multipleChoice, wordFocusListening }
|
||||
|
||||
extension ActivityTypeExtension on ActivityTypeEnum {
|
||||
String get string {
|
||||
switch (this) {
|
||||
case ActivityTypeEnum.multipleChoice:
|
||||
return 'multiple_choice';
|
||||
case ActivityTypeEnum.freeResponse:
|
||||
return 'free_response';
|
||||
case ActivityTypeEnum.listening:
|
||||
return 'listening';
|
||||
case ActivityTypeEnum.speaking:
|
||||
return 'speaking';
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
return 'word_focus_listening';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,63 +38,49 @@ enum ConstructUseTypeEnum {
|
|||
|
||||
/// was target construct in word meaning in context practice activity and incorrectly selected
|
||||
incPA,
|
||||
|
||||
/// was target lemma in word-focus listening activity and correctly selected
|
||||
corWL,
|
||||
|
||||
/// form of lemma was read-aloud in word-focus listening activity and incorrectly selected
|
||||
incWL,
|
||||
|
||||
/// form of lemma was read-aloud in word-focus listening activity and correctly ignored
|
||||
ignWL,
|
||||
|
||||
/// not defined, likely a new construct introduced by choreo and not yet classified by an old version of the client
|
||||
nan
|
||||
}
|
||||
|
||||
extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
||||
String get string {
|
||||
switch (this) {
|
||||
case ConstructUseTypeEnum.ga:
|
||||
return 'ga';
|
||||
case ConstructUseTypeEnum.wa:
|
||||
return 'wa';
|
||||
case ConstructUseTypeEnum.corIt:
|
||||
return 'corIt';
|
||||
case ConstructUseTypeEnum.incIt:
|
||||
return 'incIt';
|
||||
case ConstructUseTypeEnum.ignIt:
|
||||
return 'ignIt';
|
||||
case ConstructUseTypeEnum.ignIGC:
|
||||
return 'ignIGC';
|
||||
case ConstructUseTypeEnum.corIGC:
|
||||
return 'corIGC';
|
||||
case ConstructUseTypeEnum.incIGC:
|
||||
return 'incIGC';
|
||||
case ConstructUseTypeEnum.unk:
|
||||
return 'unk';
|
||||
case ConstructUseTypeEnum.corPA:
|
||||
return 'corPA';
|
||||
case ConstructUseTypeEnum.incPA:
|
||||
return 'incPA';
|
||||
case ConstructUseTypeEnum.ignPA:
|
||||
return 'ignPA';
|
||||
}
|
||||
}
|
||||
String get string => toString().split('.').last;
|
||||
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
case ConstructUseTypeEnum.ga:
|
||||
return Icons.check;
|
||||
case ConstructUseTypeEnum.wa:
|
||||
return Icons.thumb_up_sharp;
|
||||
|
||||
case ConstructUseTypeEnum.corIt:
|
||||
return Icons.translate;
|
||||
case ConstructUseTypeEnum.incIt:
|
||||
return Icons.translate;
|
||||
case ConstructUseTypeEnum.ignIt:
|
||||
return Icons.translate;
|
||||
|
||||
case ConstructUseTypeEnum.ignIGC:
|
||||
return Icons.close;
|
||||
case ConstructUseTypeEnum.corIGC:
|
||||
return Icons.check;
|
||||
case ConstructUseTypeEnum.incIGC:
|
||||
return Icons.close;
|
||||
case ConstructUseTypeEnum.corPA:
|
||||
return Icons.check;
|
||||
case ConstructUseTypeEnum.incPA:
|
||||
return Icons.close;
|
||||
case ConstructUseTypeEnum.ignPA:
|
||||
case ConstructUseTypeEnum.ignWL:
|
||||
case ConstructUseTypeEnum.incWL:
|
||||
return Icons.close;
|
||||
|
||||
case ConstructUseTypeEnum.ga:
|
||||
case ConstructUseTypeEnum.corIGC:
|
||||
case ConstructUseTypeEnum.corPA:
|
||||
case ConstructUseTypeEnum.corWL:
|
||||
return Icons.check;
|
||||
|
||||
case ConstructUseTypeEnum.unk:
|
||||
case ConstructUseTypeEnum.nan:
|
||||
return Icons.help;
|
||||
}
|
||||
}
|
||||
|
|
@ -107,30 +93,35 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
/// Practice activities get a moderate amount of points.
|
||||
int get pointValue {
|
||||
switch (this) {
|
||||
case ConstructUseTypeEnum.ga:
|
||||
return 2;
|
||||
case ConstructUseTypeEnum.wa:
|
||||
return 3;
|
||||
case ConstructUseTypeEnum.corIt:
|
||||
return 1;
|
||||
case ConstructUseTypeEnum.incIt:
|
||||
return -1;
|
||||
case ConstructUseTypeEnum.ignIt:
|
||||
return 1;
|
||||
case ConstructUseTypeEnum.ignIGC:
|
||||
return 1;
|
||||
case ConstructUseTypeEnum.corIGC:
|
||||
return 2;
|
||||
case ConstructUseTypeEnum.incIGC:
|
||||
return -1;
|
||||
case ConstructUseTypeEnum.unk:
|
||||
return 0;
|
||||
case ConstructUseTypeEnum.corPA:
|
||||
return 5;
|
||||
case ConstructUseTypeEnum.incPA:
|
||||
return -2;
|
||||
|
||||
case ConstructUseTypeEnum.wa:
|
||||
case ConstructUseTypeEnum.corWL:
|
||||
return 3;
|
||||
|
||||
case ConstructUseTypeEnum.ga:
|
||||
case ConstructUseTypeEnum.corIGC:
|
||||
return 2;
|
||||
|
||||
case ConstructUseTypeEnum.corIt:
|
||||
case ConstructUseTypeEnum.ignIt:
|
||||
case ConstructUseTypeEnum.ignIGC:
|
||||
case ConstructUseTypeEnum.ignPA:
|
||||
case ConstructUseTypeEnum.ignWL:
|
||||
return 1;
|
||||
|
||||
case ConstructUseTypeEnum.unk:
|
||||
case ConstructUseTypeEnum.nan:
|
||||
return 0;
|
||||
|
||||
case ConstructUseTypeEnum.incIt:
|
||||
case ConstructUseTypeEnum.incIGC:
|
||||
return -1;
|
||||
|
||||
case ConstructUseTypeEnum.incPA:
|
||||
case ConstructUseTypeEnum.incWL:
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/choreo_record.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/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
|
|
@ -37,4 +41,48 @@ extension PangeaEvent on Event {
|
|||
throw Exception("$type events do not have pangea content");
|
||||
}
|
||||
}
|
||||
|
||||
Future<PangeaAudioFile?> getPangeaAudioFile() async {
|
||||
if (type != EventTypes.Message || messageType != MessageTypes.Audio) {
|
||||
ErrorHandler.logError(
|
||||
e: "Event is not an audio message",
|
||||
data: {
|
||||
"event": toJson(),
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
final transcription =
|
||||
content.tryGetMap<String, dynamic>(ModelKey.transcription);
|
||||
final audioContent =
|
||||
content.tryGetMap<String, dynamic>('org.matrix.msc1767.audio');
|
||||
if (transcription == null || audioContent == null) {
|
||||
ErrorHandler.logError(
|
||||
e: "Called getPangeaAudioFile on an audio message without transcription or audio content",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
final matrixFile = await downloadAndDecryptAttachment();
|
||||
final duration = audioContent.tryGet<int>('duration');
|
||||
final waveform = audioContent.tryGetList<int>('waveform');
|
||||
|
||||
// old audio messages will not have tokens
|
||||
final tokensContent = transcription.tryGetList(ModelKey.tokens);
|
||||
if (tokensContent == null) return null;
|
||||
|
||||
final tokens = tokensContent
|
||||
.map((e) => TTSToken.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
return PangeaAudioFile(
|
||||
bytes: matrixFile.bytes,
|
||||
name: matrixFile.name,
|
||||
tokens: tokens,
|
||||
mimeType: matrixFile.mimeType,
|
||||
duration: duration,
|
||||
waveform: waveform,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'dart:developer';
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/constants/bot_mode.dart';
|
||||
import 'package:fluffychat/pangea/constants/class_code_constants.dart';
|
||||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ extension RoomInformationRoomExtension on Room {
|
|||
);
|
||||
}
|
||||
|
||||
Future<bool> get _isBotDM async => botOptions?.mode == BotMode.direct;
|
||||
Future<bool> get _isBotDM async => botOptions?.mode == BotMode.directChat;
|
||||
|
||||
bool get _isLocked {
|
||||
if (isDirectChat) return false;
|
||||
|
|
|
|||
|
|
@ -15,8 +15,14 @@ extension SpaceRoomExtension on Room {
|
|||
}
|
||||
return "Not in a class!";
|
||||
}
|
||||
|
||||
return canonicalAlias.replaceAll(":$domainString", "").replaceAll("#", "");
|
||||
final roomJoinRules = getState(EventTypes.RoomJoinRules, "");
|
||||
if (roomJoinRules != null) {
|
||||
final accessCode = roomJoinRules.content.tryGet(ModelKey.accessCode);
|
||||
if (accessCode is String) {
|
||||
return accessCode;
|
||||
}
|
||||
}
|
||||
return noClassCode;
|
||||
}
|
||||
|
||||
void _checkClass() {
|
||||
|
|
|
|||
|
|
@ -81,17 +81,17 @@ class PangeaMessageEvent {
|
|||
_representations = null;
|
||||
}
|
||||
|
||||
Future<PangeaAudioFile> getMatrixAudioFile(
|
||||
Future<PangeaAudioFile?> getMatrixAudioFile(
|
||||
String langCode,
|
||||
BuildContext context,
|
||||
) async {
|
||||
final String text = (await representationByLanguageGlobal(
|
||||
langCode: langCode,
|
||||
))
|
||||
?.text ??
|
||||
body;
|
||||
final RepresentationEvent? rep = representationByLanguage(langCode);
|
||||
|
||||
if (rep == null) return null;
|
||||
|
||||
final TextToSpeechRequest params = TextToSpeechRequest(
|
||||
text: text,
|
||||
text: rep.content.text,
|
||||
tokens: (await rep.tokensGlobal(context)).map((t) => t.text).toList(),
|
||||
langCode: langCode,
|
||||
);
|
||||
|
||||
|
|
@ -111,9 +111,10 @@ class PangeaMessageEvent {
|
|||
mimeType: response.mimeType,
|
||||
duration: response.durationMillis,
|
||||
waveform: response.waveform,
|
||||
tokens: response.ttsTokens,
|
||||
);
|
||||
|
||||
sendAudioEvent(file, response, text, langCode);
|
||||
sendAudioEvent(file, response, rep.text, langCode);
|
||||
|
||||
return file;
|
||||
}
|
||||
|
|
@ -137,10 +138,8 @@ class PangeaMessageEvent {
|
|||
'duration': response.durationMillis,
|
||||
'waveform': response.waveform,
|
||||
},
|
||||
ModelKey.transcription: {
|
||||
ModelKey.text: text,
|
||||
ModelKey.langCode: langCode,
|
||||
},
|
||||
ModelKey.transcription:
|
||||
response.toPangeaAudioEventData(text, langCode).toJson(),
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -155,97 +154,46 @@ class PangeaMessageEvent {
|
|||
return audioEvent;
|
||||
}
|
||||
|
||||
//get audio for text and language
|
||||
//if no audio exists, create it
|
||||
//if audio exists, return it
|
||||
Future<Event?> getTextToSpeechGlobal(String langCode) async {
|
||||
final String text = representationByLanguage(langCode)?.text ?? body;
|
||||
|
||||
final local = getTextToSpeechLocal(langCode, text);
|
||||
|
||||
if (local != null) return Future.value(local);
|
||||
|
||||
final TextToSpeechRequest params = TextToSpeechRequest(
|
||||
text: text,
|
||||
langCode: langCode,
|
||||
);
|
||||
|
||||
final TextToSpeechResponse response =
|
||||
await MatrixState.pangeaController.textToSpeech.get(
|
||||
params,
|
||||
);
|
||||
|
||||
final audioBytes = base64.decode(response.audioContent);
|
||||
|
||||
// if (!TextToSpeechController.isOggFile(audioBytes)) {
|
||||
// throw Exception("File is not a valid OGG format");
|
||||
// } else {
|
||||
// debugPrint("File is a valid OGG format");
|
||||
// }
|
||||
|
||||
// from text, trim whitespace, remove special characters, and limit to 20 characters
|
||||
// final fileName =
|
||||
// text.trim().replaceAll(RegExp('[^A-Za-z0-9]'), '').substring(0, 20);
|
||||
final eventIdParam = _event.eventId;
|
||||
final fileName =
|
||||
"audio_for_${eventIdParam}_$langCode.${response.fileExtension}";
|
||||
|
||||
final file = MatrixAudioFile(
|
||||
bytes: audioBytes,
|
||||
name: fileName,
|
||||
mimeType: response.mimeType,
|
||||
);
|
||||
|
||||
// try {
|
||||
final String? eventId = await room.sendFileEvent(
|
||||
file,
|
||||
inReplyTo: _event,
|
||||
extraContent: {
|
||||
'info': {
|
||||
...file.info,
|
||||
'duration': response.durationMillis,
|
||||
},
|
||||
'org.matrix.msc3245.voice': {},
|
||||
'org.matrix.msc1767.audio': {
|
||||
'duration': response.durationMillis,
|
||||
'waveform': response.waveform,
|
||||
},
|
||||
ModelKey.transcription: {
|
||||
ModelKey.text: text,
|
||||
ModelKey.langCode: langCode,
|
||||
},
|
||||
},
|
||||
);
|
||||
// .timeout(
|
||||
// Durations.long4,
|
||||
// onTimeout: () {
|
||||
// debugPrint("timeout in getTextToSpeechGlobal");
|
||||
// return null;
|
||||
// },
|
||||
// );
|
||||
|
||||
debugPrint("eventId in getTextToSpeechGlobal $eventId");
|
||||
return eventId != null ? room.getEventById(eventId) : null;
|
||||
}
|
||||
|
||||
Event? getTextToSpeechLocal(String langCode, String text) {
|
||||
return allAudio.firstWhereOrNull(
|
||||
(element) {
|
||||
// Safely access the transcription map
|
||||
final transcription = element.content.tryGetMap(ModelKey.transcription);
|
||||
(event) {
|
||||
try {
|
||||
// Safely access
|
||||
final dataMap = event.content.tryGetMap(ModelKey.transcription);
|
||||
|
||||
// return transcription != null;
|
||||
if (transcription == null) {
|
||||
// If transcription is null, this element does not match.
|
||||
if (dataMap == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// old text to speech content will not have TTSToken data
|
||||
// we want to disregard them and just generate new ones
|
||||
// for that, we'll return false if 'tokens' are null
|
||||
// while in-development, we'll pause here to inspect
|
||||
// debugger can be removed after we're sure it's working
|
||||
if (dataMap['tokens'] == null) {
|
||||
// events before today will definitely not have the tokens
|
||||
debugger(
|
||||
when: kDebugMode &&
|
||||
event.originServerTs.isAfter(DateTime(2024, 10, 16)),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
final PangeaAudioEventData audioData =
|
||||
PangeaAudioEventData.fromJson(dataMap as dynamic);
|
||||
|
||||
// Check if both language code and text match
|
||||
return audioData.langCode == langCode && audioData.text == text;
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: event.content.tryGetMap(ModelKey.transcription),
|
||||
m: "error parsing data in getTextToSpeechLocal",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Safely get language code and text from the transcription
|
||||
final elementLangCode = transcription[ModelKey.langCode];
|
||||
final elementText = transcription[ModelKey.text];
|
||||
|
||||
// Check if both language code and text matsch
|
||||
return elementLangCode == langCode && elementText == text;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -639,18 +587,28 @@ class PangeaMessageEvent {
|
|||
/// Returns a list of all [PracticeActivityEvent] objects
|
||||
/// associated with this message event.
|
||||
List<PracticeActivityEvent> get _practiceActivityEvents {
|
||||
return _latestEdit
|
||||
final List<Event> events = _latestEdit
|
||||
.aggregatedEvents(
|
||||
timeline,
|
||||
PangeaEventTypes.pangeaActivity,
|
||||
)
|
||||
.map(
|
||||
(e) => PracticeActivityEvent(
|
||||
timeline: timeline,
|
||||
event: e,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
final List<PracticeActivityEvent> practiceEvents = [];
|
||||
for (final event in events) {
|
||||
try {
|
||||
practiceEvents.add(
|
||||
PracticeActivityEvent(
|
||||
timeline: timeline,
|
||||
event: event,
|
||||
),
|
||||
);
|
||||
final content = practiceEvents.last.practiceActivity;
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(e: e, s: s, data: event.toJson());
|
||||
}
|
||||
}
|
||||
return practiceEvents;
|
||||
}
|
||||
|
||||
/// Returns a boolean value indicating whether there are any
|
||||
|
|
@ -668,23 +626,10 @@ class PangeaMessageEvent {
|
|||
List<PracticeActivityEvent> practiceActivitiesByLangCode(
|
||||
String langCode, {
|
||||
bool debug = false,
|
||||
}) {
|
||||
// @wcjord - disabled try catch for testing
|
||||
try {
|
||||
debugger(when: debug);
|
||||
final List<PracticeActivityEvent> activities = [];
|
||||
for (final event in _practiceActivityEvents) {
|
||||
if (event.practiceActivity.langCode == langCode) {
|
||||
activities.add(event);
|
||||
}
|
||||
}
|
||||
return activities;
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: s, data: event.toJson());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}) =>
|
||||
_practiceActivityEvents
|
||||
.where((event) => event.practiceActivity.langCode == langCode)
|
||||
.toList();
|
||||
|
||||
/// Returns a list of [PracticeActivityEvent] for the user's active l2.
|
||||
List<PracticeActivityEvent> get practiceActivities =>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ class PracticeActivityEvent {
|
|||
return _content!;
|
||||
} catch (e, s) {
|
||||
final contentMap = event.content;
|
||||
debugger(when: kDebugMode);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ class BotOptionsModel {
|
|||
bool? customTriggerReactionEnabled;
|
||||
String? customTriggerReactionKey;
|
||||
String? textAdventureGameMasterInstructions;
|
||||
String? targetLanguage;
|
||||
String? targetVoice;
|
||||
|
||||
BotOptionsModel({
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -31,6 +33,8 @@ class BotOptionsModel {
|
|||
this.keywords = const [],
|
||||
this.safetyModeration = true,
|
||||
this.mode = BotMode.discussion,
|
||||
this.targetLanguage,
|
||||
this.targetVoice,
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Discussion Mode Options
|
||||
|
|
@ -63,6 +67,8 @@ class BotOptionsModel {
|
|||
: null,
|
||||
safetyModeration: json[ModelKey.safetyModeration] ?? true,
|
||||
mode: json[ModelKey.mode] ?? BotMode.discussion,
|
||||
targetLanguage: json[ModelKey.targetLanguage],
|
||||
targetVoice: json[ModelKey.targetVoice],
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Discussion Mode Options
|
||||
|
|
@ -97,6 +103,8 @@ class BotOptionsModel {
|
|||
data[ModelKey.languageLevel] = languageLevel;
|
||||
data[ModelKey.safetyModeration] = safetyModeration;
|
||||
data[ModelKey.mode] = mode;
|
||||
data[ModelKey.targetLanguage] = targetLanguage;
|
||||
data[ModelKey.targetVoice] = targetVoice;
|
||||
data[ModelKey.discussionTopic] = discussionTopic;
|
||||
data[ModelKey.discussionKeywords] = discussionKeywords;
|
||||
data[ModelKey.discussionTriggerReactionEnabled] =
|
||||
|
|
@ -153,6 +161,12 @@ class BotOptionsModel {
|
|||
case ModelKey.textAdventureGameMasterInstructions:
|
||||
textAdventureGameMasterInstructions = value;
|
||||
break;
|
||||
case ModelKey.targetLanguage:
|
||||
targetLanguage = value;
|
||||
break;
|
||||
case ModelKey.targetVoice:
|
||||
targetVoice = value;
|
||||
break;
|
||||
default:
|
||||
throw Exception('Invalid key for bot options - $key');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,195 +1,195 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
// import 'dart:convert';
|
||||
// import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
// import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
// import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
// import 'package:flutter/foundation.dart';
|
||||
// import 'package:flutter/services.dart';
|
||||
|
||||
import '../enum/vocab_proficiency_enum.dart';
|
||||
// import '../enum/vocab_proficiency_enum.dart';
|
||||
|
||||
class VocabHeadwords {
|
||||
List<VocabList> lists;
|
||||
// class VocabHeadwords {
|
||||
// List<VocabList> lists;
|
||||
|
||||
VocabHeadwords({
|
||||
required this.lists,
|
||||
});
|
||||
// VocabHeadwords({
|
||||
// required this.lists,
|
||||
// });
|
||||
|
||||
/// in json parameter, keys are the names of the VocabList
|
||||
/// values are the words in the VocabList
|
||||
factory VocabHeadwords.fromJson(Map<String, dynamic> json) {
|
||||
final List<VocabList> lists = [];
|
||||
for (final entry in json.entries) {
|
||||
lists.add(
|
||||
VocabList(
|
||||
name: entry.key,
|
||||
lemmas: (entry.value as Iterable).cast<String>().toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return VocabHeadwords(lists: lists);
|
||||
}
|
||||
// /// in json parameter, keys are the names of the VocabList
|
||||
// /// values are the words in the VocabList
|
||||
// factory VocabHeadwords.fromJson(Map<String, dynamic> json) {
|
||||
// final List<VocabList> lists = [];
|
||||
// for (final entry in json.entries) {
|
||||
// lists.add(
|
||||
// VocabList(
|
||||
// name: entry.key,
|
||||
// lemmas: (entry.value as Iterable).cast<String>().toList(),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// return VocabHeadwords(lists: lists);
|
||||
// }
|
||||
|
||||
static Future<VocabHeadwords> getHeadwords(String langCode) async {
|
||||
final String data =
|
||||
await rootBundle.loadString('${langCode}_headwords.json');
|
||||
final decoded = jsonDecode(data);
|
||||
final VocabHeadwords headwords = VocabHeadwords.fromJson(decoded);
|
||||
return headwords;
|
||||
}
|
||||
}
|
||||
// static Future<VocabHeadwords> getHeadwords(String langCode) async {
|
||||
// final String data =
|
||||
// await rootBundle.loadString('${langCode}_headwords.json');
|
||||
// final decoded = jsonDecode(data);
|
||||
// final VocabHeadwords headwords = VocabHeadwords.fromJson(decoded);
|
||||
// return headwords;
|
||||
// }
|
||||
// }
|
||||
|
||||
class VocabList {
|
||||
String name;
|
||||
// class VocabList {
|
||||
// String name;
|
||||
|
||||
/// key is lemma
|
||||
Map<String, VocabTotals> words = {};
|
||||
// /// key is lemma
|
||||
// Map<String, VocabTotals> words = {};
|
||||
|
||||
VocabList({
|
||||
required this.name,
|
||||
required List<String> lemmas,
|
||||
}) {
|
||||
for (final lemma in lemmas) {
|
||||
words[lemma] = VocabTotals.newTotals;
|
||||
}
|
||||
}
|
||||
// VocabList({
|
||||
// required this.name,
|
||||
// required List<String> lemmas,
|
||||
// }) {
|
||||
// for (final lemma in lemmas) {
|
||||
// words[lemma] = VocabTotals.newTotals;
|
||||
// }
|
||||
// }
|
||||
|
||||
void addVocabUse(String lemma, List<OneConstructUse> use) {
|
||||
words[lemma.toUpperCase()]?.addVocabUseBasedOnUseType(use);
|
||||
}
|
||||
// void addVocabUse(String lemma, List<OneConstructUse> use) {
|
||||
// words[lemma.toUpperCase()]?.addVocabUseBasedOnUseType(use);
|
||||
// }
|
||||
|
||||
ListTotals calculuateTotals() {
|
||||
final ListTotals listTotals = ListTotals.empty;
|
||||
for (final word in words.entries) {
|
||||
debugger(when: kDebugMode && word.key == "baloncesto".toLowerCase());
|
||||
listTotals.addByType(word.value.proficiencyLevel);
|
||||
}
|
||||
return listTotals;
|
||||
}
|
||||
}
|
||||
// ListTotals calculuateTotals() {
|
||||
// final ListTotals listTotals = ListTotals.empty;
|
||||
// for (final word in words.entries) {
|
||||
// debugger(when: kDebugMode && word.key == "baloncesto".toLowerCase());
|
||||
// listTotals.addByType(word.value.proficiencyLevel);
|
||||
// }
|
||||
// return listTotals;
|
||||
// }
|
||||
// }
|
||||
|
||||
class ListTotals {
|
||||
int low;
|
||||
int medium;
|
||||
int high;
|
||||
int unknown;
|
||||
// class ListTotals {
|
||||
// int low;
|
||||
// int medium;
|
||||
// int high;
|
||||
// int unknown;
|
||||
|
||||
ListTotals({
|
||||
required this.low,
|
||||
required this.medium,
|
||||
required this.high,
|
||||
required this.unknown,
|
||||
});
|
||||
// ListTotals({
|
||||
// required this.low,
|
||||
// required this.medium,
|
||||
// required this.high,
|
||||
// required this.unknown,
|
||||
// });
|
||||
|
||||
static get empty => ListTotals(low: 0, medium: 0, high: 0, unknown: 0);
|
||||
// static get empty => ListTotals(low: 0, medium: 0, high: 0, unknown: 0);
|
||||
|
||||
void addByType(VocabProficiencyEnum prof) {
|
||||
switch (prof) {
|
||||
case VocabProficiencyEnum.low:
|
||||
low++;
|
||||
break;
|
||||
case VocabProficiencyEnum.medium:
|
||||
medium++;
|
||||
break;
|
||||
case VocabProficiencyEnum.high:
|
||||
high++;
|
||||
break;
|
||||
case VocabProficiencyEnum.unk:
|
||||
unknown++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// void addByType(VocabProficiencyEnum prof) {
|
||||
// switch (prof) {
|
||||
// case VocabProficiencyEnum.low:
|
||||
// low++;
|
||||
// break;
|
||||
// case VocabProficiencyEnum.medium:
|
||||
// medium++;
|
||||
// break;
|
||||
// case VocabProficiencyEnum.high:
|
||||
// high++;
|
||||
// break;
|
||||
// case VocabProficiencyEnum.unk:
|
||||
// unknown++;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
class VocabTotals {
|
||||
num ga;
|
||||
// class VocabTotals {
|
||||
// num ga;
|
||||
|
||||
num wa;
|
||||
// num wa;
|
||||
|
||||
num corIt;
|
||||
// num corIt;
|
||||
|
||||
num incIt;
|
||||
// num incIt;
|
||||
|
||||
num ignIt;
|
||||
// num ignIt;
|
||||
|
||||
VocabTotals({
|
||||
required this.ga,
|
||||
required this.wa,
|
||||
required this.corIt,
|
||||
required this.incIt,
|
||||
required this.ignIt,
|
||||
});
|
||||
// VocabTotals({
|
||||
// required this.ga,
|
||||
// required this.wa,
|
||||
// required this.corIt,
|
||||
// required this.incIt,
|
||||
// required this.ignIt,
|
||||
// });
|
||||
|
||||
num get calculateEstimatedVocabProficiency {
|
||||
const num gaWeight = -1;
|
||||
const num waWeight = 1;
|
||||
const num corItWeight = 0.5;
|
||||
const num incItWeight = -0.5;
|
||||
const num ignItWeight = 0.1;
|
||||
// num get calculateEstimatedVocabProficiency {
|
||||
// const num gaWeight = -1;
|
||||
// const num waWeight = 1;
|
||||
// const num corItWeight = 0.5;
|
||||
// const num incItWeight = -0.5;
|
||||
// const num ignItWeight = 0.1;
|
||||
|
||||
final num gaScore = ga * gaWeight;
|
||||
final num waScore = wa * waWeight;
|
||||
final num corItScore = corIt * corItWeight;
|
||||
final num incItScore = incIt * incItWeight;
|
||||
final num ignItScore = ignIt * ignItWeight;
|
||||
// final num gaScore = ga * gaWeight;
|
||||
// final num waScore = wa * waWeight;
|
||||
// final num corItScore = corIt * corItWeight;
|
||||
// final num incItScore = incIt * incItWeight;
|
||||
// final num ignItScore = ignIt * ignItWeight;
|
||||
|
||||
final num totalScore =
|
||||
gaScore + waScore + corItScore + incItScore + ignItScore;
|
||||
// final num totalScore =
|
||||
// gaScore + waScore + corItScore + incItScore + ignItScore;
|
||||
|
||||
return totalScore;
|
||||
}
|
||||
// return totalScore;
|
||||
// }
|
||||
|
||||
VocabProficiencyEnum get proficiencyLevel =>
|
||||
VocabProficiencyUtil.proficiency(calculateEstimatedVocabProficiency);
|
||||
// VocabProficiencyEnum get proficiencyLevel =>
|
||||
// VocabProficiencyUtil.proficiency(calculateEstimatedVocabProficiency);
|
||||
|
||||
static VocabTotals get newTotals {
|
||||
return VocabTotals(
|
||||
ga: 0,
|
||||
wa: 0,
|
||||
corIt: 0,
|
||||
incIt: 0,
|
||||
ignIt: 0,
|
||||
);
|
||||
}
|
||||
// static VocabTotals get newTotals {
|
||||
// return VocabTotals(
|
||||
// ga: 0,
|
||||
// wa: 0,
|
||||
// corIt: 0,
|
||||
// incIt: 0,
|
||||
// ignIt: 0,
|
||||
// );
|
||||
// }
|
||||
|
||||
void addVocabUseBasedOnUseType(List<OneConstructUse> uses) {
|
||||
for (final use in uses) {
|
||||
switch (use.useType) {
|
||||
case ConstructUseTypeEnum.ga:
|
||||
ga++;
|
||||
break;
|
||||
case ConstructUseTypeEnum.wa:
|
||||
wa++;
|
||||
break;
|
||||
case ConstructUseTypeEnum.corIt:
|
||||
corIt++;
|
||||
break;
|
||||
case ConstructUseTypeEnum.incIt:
|
||||
incIt++;
|
||||
break;
|
||||
case ConstructUseTypeEnum.ignIt:
|
||||
ignIt++;
|
||||
break;
|
||||
//TODO - these shouldn't be counted as such
|
||||
case ConstructUseTypeEnum.ignIGC:
|
||||
ignIt++;
|
||||
break;
|
||||
case ConstructUseTypeEnum.corIGC:
|
||||
corIt++;
|
||||
break;
|
||||
case ConstructUseTypeEnum.incIGC:
|
||||
incIt++;
|
||||
break;
|
||||
//TODO if we bring back Headwords then we need to add these
|
||||
case ConstructUseTypeEnum.corPA:
|
||||
break;
|
||||
case ConstructUseTypeEnum.incPA:
|
||||
break;
|
||||
case ConstructUseTypeEnum.unk:
|
||||
break;
|
||||
case ConstructUseTypeEnum.ignPA:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// void addVocabUseBasedOnUseType(List<OneConstructUse> uses) {
|
||||
// for (final use in uses) {
|
||||
// switch (use.useType) {
|
||||
// case ConstructUseTypeEnum.ga:
|
||||
// ga++;
|
||||
// break;
|
||||
// case ConstructUseTypeEnum.wa:
|
||||
// wa++;
|
||||
// break;
|
||||
// case ConstructUseTypeEnum.corIt:
|
||||
// corIt++;
|
||||
// break;
|
||||
// case ConstructUseTypeEnum.incIt:
|
||||
// incIt++;
|
||||
// break;
|
||||
// case ConstructUseTypeEnum.ignIt:
|
||||
// ignIt++;
|
||||
// break;
|
||||
// //TODO - these shouldn't be counted as such
|
||||
// case ConstructUseTypeEnum.ignIGC:
|
||||
// ignIt++;
|
||||
// break;
|
||||
// case ConstructUseTypeEnum.corIGC:
|
||||
// corIt++;
|
||||
// break;
|
||||
// case ConstructUseTypeEnum.incIGC:
|
||||
// incIt++;
|
||||
// break;
|
||||
// //TODO if we bring back Headwords then we need to add these
|
||||
// case ConstructUseTypeEnum.corPA:
|
||||
// break;
|
||||
// case ConstructUseTypeEnum.incPA:
|
||||
// break;
|
||||
// case ConstructUseTypeEnum.unk:
|
||||
// break;
|
||||
// case ConstructUseTypeEnum.ignPA:
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -128,8 +128,6 @@ class PangeaToken {
|
|||
lemma: lemma.text,
|
||||
type: ConstructTypeEnum.vocab,
|
||||
),
|
||||
xp: 0,
|
||||
lastUsed: null,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -140,8 +138,6 @@ class PangeaToken {
|
|||
lemma: morph.key,
|
||||
type: ConstructTypeEnum.morph,
|
||||
),
|
||||
xp: 0,
|
||||
lastUsed: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
|
||||
|
|
@ -7,11 +8,13 @@ class ConstructWithXP {
|
|||
final ConstructIdentifier id;
|
||||
int xp;
|
||||
DateTime? lastUsed;
|
||||
List<ConstructUseTypeEnum> condensedConstructUses;
|
||||
|
||||
ConstructWithXP({
|
||||
required this.id,
|
||||
required this.xp,
|
||||
required this.lastUsed,
|
||||
this.xp = 0,
|
||||
this.lastUsed,
|
||||
this.condensedConstructUses = const [],
|
||||
});
|
||||
|
||||
factory ConstructWithXP.fromJson(Map<String, dynamic> json) {
|
||||
|
|
@ -23,6 +26,14 @@ class ConstructWithXP {
|
|||
lastUsed: json['last_used'] != null
|
||||
? 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;
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -31,6 +42,7 @@ class ConstructWithXP {
|
|||
'construct_id': id.toJson(),
|
||||
'xp': xp,
|
||||
'last_used': lastUsed?.toIso8601String(),
|
||||
'uses': condensedConstructUses.map((e) => e.string).toList(),
|
||||
};
|
||||
return json;
|
||||
}
|
||||
|
|
@ -230,6 +242,11 @@ class MessageActivityRequest {
|
|||
'existing_activities': existingActivities.map((e) => e.toJson()).toList(),
|
||||
'activity_quality_feedback': activityQualityFeedback?.toJson(),
|
||||
'iso_8601_time_of_req': DateTime.now().toIso8601String(),
|
||||
// this is a list of activity types that the client can handle
|
||||
// the server will only return activities of these types
|
||||
// this for backwards compatibility with old clients
|
||||
'client_version_compatible_activity_types':
|
||||
ActivityTypeEnum.values.map((e) => e.string).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activ
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MultipleChoice {
|
||||
class ActivityContent {
|
||||
final String question;
|
||||
final List<String> choices;
|
||||
final String answer;
|
||||
final RelevantSpanDisplayDetails? spanDisplayDetails;
|
||||
|
||||
MultipleChoice({
|
||||
ActivityContent({
|
||||
required this.question,
|
||||
required this.choices,
|
||||
required this.answer,
|
||||
|
|
@ -37,12 +37,12 @@ class MultipleChoice {
|
|||
Color choiceColor(int index) =>
|
||||
index == correctAnswerIndex ? AppConfig.success : AppConfig.warning;
|
||||
|
||||
factory MultipleChoice.fromJson(Map<String, dynamic> json) {
|
||||
factory ActivityContent.fromJson(Map<String, dynamic> json) {
|
||||
final spanDisplay = json['span_display_details'] != null &&
|
||||
json['span_display_details'] is Map
|
||||
? RelevantSpanDisplayDetails.fromJson(json['span_display_details'])
|
||||
: null;
|
||||
return MultipleChoice(
|
||||
return ActivityContent(
|
||||
question: json['question'] as String,
|
||||
choices: (json['choices'] as List).map((e) => e as String).toList(),
|
||||
answer: json['answer'] ?? json['correct_answer'] as String,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
|||
import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
class ConstructIdentifier {
|
||||
final String lemma;
|
||||
|
|
@ -165,110 +166,37 @@ class PracticeActivityRequest {
|
|||
}
|
||||
}
|
||||
|
||||
class FreeResponse {
|
||||
final String question;
|
||||
final String correctAnswer;
|
||||
final String gradingGuide;
|
||||
|
||||
FreeResponse({
|
||||
required this.question,
|
||||
required this.correctAnswer,
|
||||
required this.gradingGuide,
|
||||
});
|
||||
|
||||
factory FreeResponse.fromJson(Map<String, dynamic> json) {
|
||||
return FreeResponse(
|
||||
question: json['question'] as String,
|
||||
correctAnswer: json['correct_answer'] as String,
|
||||
gradingGuide: json['grading_guide'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'question': question,
|
||||
'correct_answer': correctAnswer,
|
||||
'grading_guide': gradingGuide,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Listening {
|
||||
final String audioUrl;
|
||||
final String text;
|
||||
|
||||
Listening({required this.audioUrl, required this.text});
|
||||
|
||||
factory Listening.fromJson(Map<String, dynamic> json) {
|
||||
return Listening(
|
||||
audioUrl: json['audio_url'] as String,
|
||||
text: json['text'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'audio_url': audioUrl,
|
||||
'text': text,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Speaking {
|
||||
final String text;
|
||||
|
||||
Speaking({required this.text});
|
||||
|
||||
factory Speaking.fromJson(Map<String, dynamic> json) {
|
||||
return Speaking(
|
||||
text: json['text'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'text': text,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class PracticeActivityModel {
|
||||
final List<ConstructIdentifier> tgtConstructs;
|
||||
final String langCode;
|
||||
final String msgId;
|
||||
final ActivityTypeEnum activityType;
|
||||
final MultipleChoice? multipleChoice;
|
||||
final Listening? listening;
|
||||
final Speaking? speaking;
|
||||
final FreeResponse? freeResponse;
|
||||
final ActivityContent content;
|
||||
|
||||
PracticeActivityModel({
|
||||
required this.tgtConstructs,
|
||||
required this.langCode,
|
||||
required this.msgId,
|
||||
required this.activityType,
|
||||
this.multipleChoice,
|
||||
this.listening,
|
||||
this.speaking,
|
||||
this.freeResponse,
|
||||
required this.content,
|
||||
});
|
||||
|
||||
String get question {
|
||||
switch (activityType) {
|
||||
case ActivityTypeEnum.multipleChoice:
|
||||
return multipleChoice!.question;
|
||||
case ActivityTypeEnum.listening:
|
||||
return listening!.text;
|
||||
case ActivityTypeEnum.speaking:
|
||||
return speaking!.text;
|
||||
case ActivityTypeEnum.freeResponse:
|
||||
return freeResponse!.question;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
String get question => content.question;
|
||||
|
||||
factory PracticeActivityModel.fromJson(Map<String, dynamic> json) {
|
||||
// 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 =
|
||||
(json['content'] ?? json["multiple_choice"]) as Map<String, dynamic>?;
|
||||
|
||||
if (content == null) {
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(data: {"json": json}),
|
||||
);
|
||||
throw ("content is null in PracticeActivityModel.fromJson");
|
||||
}
|
||||
|
||||
return PracticeActivityModel(
|
||||
tgtConstructs: ((json['tgt_constructs'] ?? json['target_constructs'])
|
||||
as List)
|
||||
|
|
@ -283,27 +211,14 @@ class PracticeActivityModel {
|
|||
e.string == json['activity_type'] as String ||
|
||||
e.string.split('.').last == json['activity_type'] as String,
|
||||
),
|
||||
multipleChoice: json['multiple_choice'] != null
|
||||
? MultipleChoice.fromJson(
|
||||
json['multiple_choice'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
listening: json['listening'] != null
|
||||
? Listening.fromJson(json['listening'] as Map<String, dynamic>)
|
||||
: null,
|
||||
speaking: json['speaking'] != null
|
||||
? Speaking.fromJson(json['speaking'] as Map<String, dynamic>)
|
||||
: null,
|
||||
freeResponse: json['free_response'] != null
|
||||
? FreeResponse.fromJson(
|
||||
json['free_response'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
content: ActivityContent.fromJson(
|
||||
content,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
RelevantSpanDisplayDetails? get relevantSpanDisplayDetails =>
|
||||
multipleChoice?.spanDisplayDetails;
|
||||
content.spanDisplayDetails;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
|
|
@ -311,10 +226,7 @@ class PracticeActivityModel {
|
|||
'lang_code': langCode,
|
||||
'msg_id': msgId,
|
||||
'activity_type': activityType.string,
|
||||
'multiple_choice': multipleChoice?.toJson(),
|
||||
'listening': listening?.toJson(),
|
||||
'speaking': speaking?.toJson(),
|
||||
'free_response': freeResponse?.toJson(),
|
||||
'content': content.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -328,10 +240,7 @@ class PracticeActivityModel {
|
|||
other.langCode == langCode &&
|
||||
other.msgId == msgId &&
|
||||
other.activityType == activityType &&
|
||||
other.multipleChoice == multipleChoice &&
|
||||
other.listening == listening &&
|
||||
other.speaking == speaking &&
|
||||
other.freeResponse == freeResponse;
|
||||
other.content == content;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -340,10 +249,7 @@ class PracticeActivityModel {
|
|||
langCode.hashCode ^
|
||||
msgId.hashCode ^
|
||||
activityType.hashCode ^
|
||||
multipleChoice.hashCode ^
|
||||
listening.hashCode ^
|
||||
speaking.hashCode ^
|
||||
freeResponse.hashCode;
|
||||
content.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -372,7 +278,7 @@ class RelevantSpanDisplayDetails {
|
|||
return RelevantSpanDisplayDetails(
|
||||
offset: json['offset'] as int,
|
||||
length: json['length'] as int,
|
||||
displayInstructions: display ?? ActivityDisplayInstructionsEnum.hide,
|
||||
displayInstructions: display ?? ActivityDisplayInstructionsEnum.nothing,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -384,7 +290,6 @@ class RelevantSpanDisplayDetails {
|
|||
};
|
||||
}
|
||||
|
||||
// override operator == and hashCode
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
|
|
|||
|
|
@ -269,4 +269,16 @@ extension SettingCopy on ToolSetting {
|
|||
return L10n.of(context)!.autoIGCToolDescription;
|
||||
}
|
||||
}
|
||||
|
||||
bool get isAvailableSetting {
|
||||
switch (this) {
|
||||
case ToolSetting.interactiveTranslator:
|
||||
case ToolSetting.interactiveGrammar:
|
||||
case ToolSetting.definitions:
|
||||
return false;
|
||||
case ToolSetting.immersionMode:
|
||||
case ToolSetting.autoIGC:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@ import 'package:fluffychat/widgets/matrix.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingsLearning extends StatefulWidget {
|
||||
const SettingsLearning({super.key});
|
||||
final bool isPopup;
|
||||
const SettingsLearning({
|
||||
this.isPopup = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
SettingsLearningController createState() => SettingsLearningController();
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@ class SettingsLearningView extends StatelessWidget {
|
|||
title: Text(
|
||||
L10n.of(context)!.learningSettings,
|
||||
),
|
||||
leading: controller.widget.isPopup
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
body: ListTileTheme(
|
||||
iconColor: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
|
|
@ -28,22 +34,12 @@ class SettingsLearningView extends StatelessWidget {
|
|||
children: [
|
||||
LanguageTile(controller),
|
||||
CountryPickerTile(controller),
|
||||
const SizedBox(height: 8),
|
||||
const Divider(height: 1),
|
||||
const SizedBox(height: 8),
|
||||
// if (controller.pangeaController.permissionsController.isUser18())
|
||||
// SwitchListTile.adaptive(
|
||||
// activeColor: AppConfig.activeToggleColor,
|
||||
// title: Text(L10n.of(context)!.publicProfileTitle),
|
||||
// subtitle: Text(L10n.of(context)!.publicProfileDesc),
|
||||
// value: controller.pangeaController.userController.isPublic,
|
||||
// onChanged: (bool isPublicProfile) =>
|
||||
// controller.setPublicProfile(isPublicProfile),
|
||||
// ),
|
||||
ListTile(
|
||||
subtitle: Text(L10n.of(context)!.toggleToolSettingsDescription),
|
||||
title: Text(L10n.of(context)!.toggleToolSettingsDescription),
|
||||
),
|
||||
for (final toolSetting in ToolSetting.values)
|
||||
for (final toolSetting in ToolSetting.values
|
||||
.where((tool) => tool.isAvailableSetting))
|
||||
ProfileSettingsSwitchListTile.adaptive(
|
||||
defaultValue: controller.getToolSetting(toolSetting),
|
||||
title: toolSetting.toolName(context),
|
||||
|
|
@ -66,18 +62,18 @@ class SettingsLearningView extends StatelessWidget {
|
|||
return profile;
|
||||
}),
|
||||
),
|
||||
ProfileSettingsSwitchListTile.adaptive(
|
||||
defaultValue: controller.pangeaController.userController.profile
|
||||
.userSettings.autoPlayMessages,
|
||||
title: L10n.of(context)!.autoPlayTitle,
|
||||
subtitle: L10n.of(context)!.autoPlayDesc,
|
||||
onChange: (bool value) => controller
|
||||
.pangeaController.userController
|
||||
.updateProfile((profile) {
|
||||
profile.userSettings.autoPlayMessages = value;
|
||||
return profile;
|
||||
}),
|
||||
),
|
||||
// ProfileSettingsSwitchListTile.adaptive(
|
||||
// defaultValue: controller.pangeaController.userController.profile
|
||||
// .userSettings.autoPlayMessages,
|
||||
// title: L10n.of(context)!.autoPlayTitle,
|
||||
// subtitle: L10n.of(context)!.autoPlayDesc,
|
||||
// onChange: (bool value) => controller
|
||||
// .pangeaController.userController
|
||||
// .updateProfile((profile) {
|
||||
// profile.userSettings.autoPlayMessages = value;
|
||||
// return profile;
|
||||
// }),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
||||
import 'package:fluffychat/pangea/constants/local.key.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -17,6 +19,7 @@ void chatListHandleSpaceTap(
|
|||
ChatListController controller,
|
||||
Room space,
|
||||
) {
|
||||
final PangeaController pangeaController = MatrixState.pangeaController;
|
||||
void setActiveSpaceAndCloseChat() {
|
||||
controller.setActiveSpace(space.id);
|
||||
|
||||
|
|
@ -105,8 +108,13 @@ void chatListHandleSpaceTap(
|
|||
(element) =>
|
||||
element.isSpace && element.membership == Membership.join,
|
||||
);
|
||||
final justInputtedCode = pangeaController.pStoreService
|
||||
.read(PLocalKey.justInputtedCode, isAccountData: false);
|
||||
if (rooms.any((s) => s.spaceChildren.any((c) => c.roomId == space.id))) {
|
||||
autoJoin(space);
|
||||
} else if (justInputtedCode != null &&
|
||||
justInputtedCode == space.classCode) {
|
||||
// do nothing
|
||||
} else {
|
||||
showAlertDialog(context);
|
||||
}
|
||||
|
|
|
|||
267
lib/pangea/utils/get_grammar_copy.dart
Normal file
267
lib/pangea/utils/get_grammar_copy.dart
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
String getGrammarCopy(String tag, BuildContext context) {
|
||||
switch (tag) {
|
||||
case 'SCONJ':
|
||||
return L10n.of(context)!.grammarCopySCONJ;
|
||||
case 'NUM':
|
||||
return L10n.of(context)!.grammarCopyNUM;
|
||||
case 'VERB':
|
||||
return L10n.of(context)!.grammarCopyVERB;
|
||||
case 'AFFIX':
|
||||
return L10n.of(context)!.grammarCopyAFFIX;
|
||||
case 'PART':
|
||||
return L10n.of(context)!.grammarCopyPARTpos;
|
||||
case 'ADJ':
|
||||
return L10n.of(context)!.grammarCopyADJ;
|
||||
case 'CCONJ':
|
||||
return L10n.of(context)!.grammarCopyCCONJ;
|
||||
case 'PUNCT':
|
||||
return L10n.of(context)!.grammarCopyPUNCT;
|
||||
case 'ADV':
|
||||
return L10n.of(context)!.grammarCopyADV;
|
||||
case 'AUX':
|
||||
return L10n.of(context)!.grammarCopyAUX;
|
||||
case 'SPACE':
|
||||
return L10n.of(context)!.grammarCopySPACE;
|
||||
case 'SYM':
|
||||
return L10n.of(context)!.grammarCopySYM;
|
||||
case 'DET':
|
||||
return L10n.of(context)!.grammarCopyDET;
|
||||
case 'PRON':
|
||||
return L10n.of(context)!.grammarCopyPRON;
|
||||
case 'ADP':
|
||||
return L10n.of(context)!.grammarCopyADP;
|
||||
case 'PROPN':
|
||||
return L10n.of(context)!.grammarCopyPROPN;
|
||||
case 'NOUN':
|
||||
return L10n.of(context)!.grammarCopyNOUN;
|
||||
case 'INTJ':
|
||||
return L10n.of(context)!.grammarCopyINTJ;
|
||||
case 'X':
|
||||
return L10n.of(context)!.grammarCopyX;
|
||||
case 'Fem':
|
||||
return L10n.of(context)!.grammarCopyFem;
|
||||
case '2':
|
||||
return L10n.of(context)!.grammarCopy2;
|
||||
case 'Imp':
|
||||
return L10n.of(context)!.grammarCopyImp;
|
||||
case 'Qest':
|
||||
return L10n.of(context)!.grammarCopyQest;
|
||||
case 'Perf':
|
||||
return L10n.of(context)!.grammarCopyPerf;
|
||||
case 'Acc,Nom':
|
||||
return L10n.of(context)!.grammarCopyAccNom;
|
||||
case 'Obl':
|
||||
return L10n.of(context)!.grammarCopyObl;
|
||||
case 'Act':
|
||||
return L10n.of(context)!.grammarCopyAct;
|
||||
case 'Brck':
|
||||
return L10n.of(context)!.grammarCopyBrck;
|
||||
case 'Art':
|
||||
return L10n.of(context)!.grammarCopyArt;
|
||||
case 'Sing':
|
||||
return L10n.of(context)!.grammarCopySing;
|
||||
case 'Masc':
|
||||
return L10n.of(context)!.grammarCopyMasc;
|
||||
case 'Mod':
|
||||
return L10n.of(context)!.grammarCopyMod;
|
||||
case 'Adverbial':
|
||||
return L10n.of(context)!.grammarCopyAdverbial;
|
||||
case 'Peri':
|
||||
return L10n.of(context)!.grammarCopyPeri;
|
||||
case 'Digit':
|
||||
return L10n.of(context)!.grammarCopyDigit;
|
||||
case 'Not_proper':
|
||||
return L10n.of(context)!.grammarCopyNot_proper;
|
||||
case 'Card':
|
||||
return L10n.of(context)!.grammarCopyCard;
|
||||
case 'Prop':
|
||||
return L10n.of(context)!.grammarCopyProp;
|
||||
case 'Dash':
|
||||
return L10n.of(context)!.grammarCopyDash;
|
||||
case 'Yes':
|
||||
return L10n.of(context)!.grammarCopyYes;
|
||||
case 'Semi':
|
||||
return L10n.of(context)!.grammarCopySemi;
|
||||
case 'Comm':
|
||||
return L10n.of(context)!.grammarCopyComm;
|
||||
case 'Cnd':
|
||||
return L10n.of(context)!.grammarCopyCnd;
|
||||
case 'Int,Rel':
|
||||
return L10n.of(context)!.grammarCopyIntRel;
|
||||
case 'Acc':
|
||||
return L10n.of(context)!.grammarCopyAcc;
|
||||
case 'Part':
|
||||
return L10n.of(context)!
|
||||
.grammarCopyPartTag; // To avoid conflict with 'PART' POS
|
||||
case 'Int':
|
||||
return L10n.of(context)!.grammarCopyInt;
|
||||
case 'Past':
|
||||
return L10n.of(context)!.grammarCopyPast;
|
||||
case 'Sup':
|
||||
return L10n.of(context)!.grammarCopySup;
|
||||
case 'Colo':
|
||||
return L10n.of(context)!.grammarCopyColo;
|
||||
case '3':
|
||||
return L10n.of(context)!.grammarCopy3;
|
||||
case 'Plur':
|
||||
return L10n.of(context)!.grammarCopyPlur;
|
||||
case 'Npr':
|
||||
return L10n.of(context)!.grammarCopyNpr;
|
||||
case 'Interrogative':
|
||||
return L10n.of(context)!.grammarCopyInterrogative;
|
||||
case 'Infm':
|
||||
return L10n.of(context)!.grammarCopyInfm;
|
||||
case 'Tim':
|
||||
return L10n.of(context)!.grammarCopyTim;
|
||||
case 'Neg':
|
||||
return L10n.of(context)!.grammarCopyNeg;
|
||||
case 'Tot':
|
||||
return L10n.of(context)!.grammarCopyTot;
|
||||
case 'Adnomial':
|
||||
return L10n.of(context)!.grammarCopyAdnomial;
|
||||
case 'Prog':
|
||||
return L10n.of(context)!.grammarCopyProg;
|
||||
case 'Sub':
|
||||
return L10n.of(context)!.grammarCopySub;
|
||||
case 'Complementive':
|
||||
return L10n.of(context)!.grammarCopyComplementive;
|
||||
case 'Nom':
|
||||
return L10n.of(context)!.grammarCopyNom;
|
||||
case 'Fut':
|
||||
return L10n.of(context)!.grammarCopyFut;
|
||||
case 'Dat':
|
||||
return L10n.of(context)!.grammarCopyDat;
|
||||
case 'Pres':
|
||||
return L10n.of(context)!.grammarCopyPres;
|
||||
case 'Neut':
|
||||
return L10n.of(context)!.grammarCopyNeut;
|
||||
case 'Rel':
|
||||
return L10n.of(context)!.grammarCopyRel;
|
||||
case 'Final_ending':
|
||||
return L10n.of(context)!.grammarCopyFinal_ending;
|
||||
case 'Dem':
|
||||
return L10n.of(context)!.grammarCopyDem;
|
||||
case 'Pre':
|
||||
return L10n.of(context)!.grammarCopyPre;
|
||||
case 'Fin':
|
||||
return L10n.of(context)!.grammarCopyFin;
|
||||
case 'Pos':
|
||||
return L10n.of(context)!.grammarCopyPos;
|
||||
case 'Quot':
|
||||
return L10n.of(context)!.grammarCopyQuot;
|
||||
case 'Ger':
|
||||
return L10n.of(context)!.grammarCopyGer;
|
||||
case 'Pass':
|
||||
return L10n.of(context)!.grammarCopyPass;
|
||||
case 'Gen':
|
||||
return L10n.of(context)!.grammarCopyGen;
|
||||
case 'Prs':
|
||||
return L10n.of(context)!.grammarCopyPrs;
|
||||
case 'Def':
|
||||
return L10n.of(context)!.grammarCopyDef;
|
||||
case 'Ord':
|
||||
return L10n.of(context)!.grammarCopyOrd;
|
||||
case 'Ins':
|
||||
return L10n.of(context)!.grammarCopyIns;
|
||||
case 'Acc,Dat':
|
||||
return L10n.of(context)!.grammarCopyAccDat;
|
||||
case 'Inf':
|
||||
return L10n.of(context)!.grammarCopyInf;
|
||||
case 'Long':
|
||||
return L10n.of(context)!.grammarCopyLong;
|
||||
case 'Ind':
|
||||
return L10n.of(context)!.grammarCopyInd;
|
||||
case 'Cmp':
|
||||
return L10n.of(context)!.grammarCopyCmp;
|
||||
case 'Relative_case':
|
||||
return L10n.of(context)!.grammarCopyRelative_case;
|
||||
case 'Excl':
|
||||
return L10n.of(context)!.grammarCopyExcl;
|
||||
case '1':
|
||||
return L10n.of(context)!.grammarCopy1;
|
||||
case 'Ini':
|
||||
return L10n.of(context)!.grammarCopyIni;
|
||||
case 'Person':
|
||||
return L10n.of(context)!.grammarCopyPerson;
|
||||
case 'Foreign':
|
||||
return L10n.of(context)!.grammarCopyForeign;
|
||||
case 'Voice':
|
||||
return L10n.of(context)!.grammarCopyVoice;
|
||||
case 'VerbType':
|
||||
return L10n.of(context)!.grammarCopyVerbType;
|
||||
case 'Poss':
|
||||
return L10n.of(context)!.grammarCopyPoss;
|
||||
case 'PrepCase':
|
||||
return L10n.of(context)!.grammarCopyPrepCase;
|
||||
case 'NumType':
|
||||
return L10n.of(context)!.grammarCopyNumType;
|
||||
case 'NounType':
|
||||
return L10n.of(context)!.grammarCopyNounType;
|
||||
case 'Reflex':
|
||||
return L10n.of(context)!.grammarCopyReflex;
|
||||
case 'PronType':
|
||||
return L10n.of(context)!.grammarCopyPronType;
|
||||
case 'PunctSide':
|
||||
return L10n.of(context)!.grammarCopyPunctSide;
|
||||
case 'VerbForm':
|
||||
return L10n.of(context)!.grammarCopyVerbForm;
|
||||
case 'Gender':
|
||||
return L10n.of(context)!.grammarCopyGender;
|
||||
case 'Mood':
|
||||
return L10n.of(context)!.grammarCopyMood;
|
||||
case 'Aspect':
|
||||
return L10n.of(context)!.grammarCopyAspect;
|
||||
case 'PunctType':
|
||||
return L10n.of(context)!.grammarCopyPunctType;
|
||||
case 'Tense':
|
||||
return L10n.of(context)!.grammarCopyTense;
|
||||
case 'Degree':
|
||||
return L10n.of(context)!.grammarCopyDegree;
|
||||
case 'Polite':
|
||||
return L10n.of(context)!.grammarCopyPolite;
|
||||
case 'AdvType':
|
||||
return L10n.of(context)!.grammarCopyAdvType;
|
||||
case 'Number':
|
||||
return L10n.of(context)!.grammarCopyNumber;
|
||||
case 'ConjType':
|
||||
return L10n.of(context)!.grammarCopyConjType;
|
||||
case 'Polarity':
|
||||
return L10n.of(context)!.grammarCopyPolarity;
|
||||
case 'Number[psor]':
|
||||
return L10n.of(context)!.grammarCopyNumberPsor;
|
||||
case 'Case':
|
||||
return L10n.of(context)!.grammarCopyCase;
|
||||
case 'Definite':
|
||||
return L10n.of(context)!.grammarCopyDefinite;
|
||||
case 'NumForm':
|
||||
return L10n.of(context)!.grammarCopyNumForm;
|
||||
// Handle empty tag
|
||||
case '':
|
||||
ErrorHandler.logError(
|
||||
e: Exception('Empty tag'),
|
||||
m: 'Empty tag in getGrammarCopy',
|
||||
data: {
|
||||
'context': context,
|
||||
},
|
||||
);
|
||||
return L10n.of(context)!.grammarCopyUnknown;
|
||||
default:
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: Exception('Need to add copy for $tag to intl_en.arb'),
|
||||
m: 'Need to add copy for $tag to intl_en.arb',
|
||||
data: {
|
||||
'tag': tag,
|
||||
'context': context,
|
||||
},
|
||||
);
|
||||
return tag; // Fallback to the tag itself if no match is found
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,38 @@
|
|||
import 'dart:math';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../controllers/pangea_controller.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class SpaceCodeUtil {
|
||||
static const codeLength = 6;
|
||||
static const codeLength = 7;
|
||||
|
||||
static bool isValidCode(String? spacecode) {
|
||||
return spacecode == null || spacecode.length > 4;
|
||||
if (spacecode == null) return false;
|
||||
return spacecode.length == codeLength && spacecode.contains(r'[0-9]');
|
||||
}
|
||||
|
||||
static String generateSpaceCode() {
|
||||
final r = Random();
|
||||
const chars = 'AaBbCcDdEeFfGgHhiJjKkLMmNnoPpQqRrSsTtUuVvWwXxYyZz1234567890';
|
||||
return List.generate(codeLength, (index) => chars[r.nextInt(chars.length)])
|
||||
.join();
|
||||
static Future<String> generateSpaceCode(Client client) async {
|
||||
final response = await client.httpClient.get(
|
||||
Uri.parse(
|
||||
'${client.homeserver}/_synapse/client/pangea/v1/request_room_code',
|
||||
),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ${client.accessToken}',
|
||||
},
|
||||
);
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to generate room code: $response');
|
||||
}
|
||||
final roomCodeResult = jsonDecode(response.body);
|
||||
if (roomCodeResult['access_code'] is String) {
|
||||
return roomCodeResult['access_code'] as String;
|
||||
} else {
|
||||
throw Exception('Invalid response, access_code not found $response');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> joinWithSpaceCodeDialog(
|
||||
|
|
@ -58,13 +73,4 @@ class SpaceCodeUtil {
|
|||
],
|
||||
),
|
||||
);
|
||||
|
||||
static void messageSnack(BuildContext context, String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
duration: const Duration(seconds: 10),
|
||||
content: Text(message),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
import 'dart:developer';
|
||||
import 'dart:math';
|
||||
|
||||
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';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
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';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
|
@ -12,11 +20,13 @@ import 'package:matrix/matrix.dart';
|
|||
class MessageAudioCard extends StatefulWidget {
|
||||
final PangeaMessageEvent messageEvent;
|
||||
final MessageOverlayController overlayController;
|
||||
final PangeaTokenText? selection;
|
||||
|
||||
const MessageAudioCard({
|
||||
super.key,
|
||||
required this.messageEvent,
|
||||
required this.overlayController,
|
||||
this.selection,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -25,9 +35,107 @@ class MessageAudioCard extends StatefulWidget {
|
|||
|
||||
class MessageAudioCardState extends State<MessageAudioCard> {
|
||||
bool _isLoading = false;
|
||||
Event? localAudioEvent;
|
||||
PangeaAudioFile? audioFile;
|
||||
|
||||
int? sectionStartMS;
|
||||
int? sectionEndMS;
|
||||
|
||||
TtsController tts = TtsController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
fetchAudio();
|
||||
|
||||
// initializeTTS();
|
||||
}
|
||||
|
||||
// initializeTTS() async {
|
||||
// tts.setupTTS().then((value) => setState(() {}));
|
||||
// }
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant oldWidget) {
|
||||
if (oldWidget.selection != widget.selection) {
|
||||
debugPrint('selection changed');
|
||||
setSectionStartAndEndFromSelection();
|
||||
playSelectionAudio();
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
Future<void> playSelectionAudio() async {
|
||||
final PangeaTokenText selection = widget.selection!;
|
||||
final tokenText = selection.content;
|
||||
|
||||
await tts.speak(tokenText);
|
||||
}
|
||||
|
||||
void setSectionStartAndEnd(int? start, int? end) => mounted
|
||||
? setState(() {
|
||||
sectionStartMS = start;
|
||||
sectionEndMS = end;
|
||||
})
|
||||
: null;
|
||||
|
||||
void setSectionStartAndEndFromSelection() async {
|
||||
if (audioFile == null) {
|
||||
// should never happen but just in case
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFile!.duration == null) {
|
||||
// should never happen but just in case
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: Exception(),
|
||||
m: 'audioFile duration is null in MessageAudioCardState',
|
||||
data: {
|
||||
'audioFile': audioFile,
|
||||
},
|
||||
);
|
||||
return setSectionStartAndEnd(null, null);
|
||||
}
|
||||
|
||||
// if there is no selection, we don't need to do anything
|
||||
// but clear the section start and end
|
||||
if (widget.selection == null) {
|
||||
return setSectionStartAndEnd(null, null);
|
||||
}
|
||||
|
||||
final PangeaTokenText selection = widget.selection!;
|
||||
final List<TTSToken> tokens = audioFile!.tokens;
|
||||
|
||||
// find the token that corresponds to the selection
|
||||
// set the start to the start of the token
|
||||
// set the end to the start of the next token or to the duration of the audio if
|
||||
// if there is no next token
|
||||
for (int i = 0; i < tokens.length; i++) {
|
||||
final TTSToken ttsToken = tokens[i];
|
||||
if (ttsToken.text.offset == selection.offset) {
|
||||
return setSectionStartAndEnd(
|
||||
max(ttsToken.startMS - 150, 0),
|
||||
min(ttsToken.endMS + 150, audioFile!.duration!),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 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',
|
||||
data: {
|
||||
'selection': selection,
|
||||
'tokens': tokens,
|
||||
'sttTokens': audioFile!.tokens,
|
||||
},
|
||||
);
|
||||
|
||||
setSectionStartAndEnd(null, null);
|
||||
}
|
||||
|
||||
Future<void> fetchAudio() async {
|
||||
if (!mounted) return;
|
||||
setState(() => _isLoading = true);
|
||||
|
|
@ -36,20 +144,27 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
final String langCode = widget.messageEvent.messageDisplayLangCode;
|
||||
final String? text =
|
||||
widget.messageEvent.representationByLanguage(langCode)?.text;
|
||||
if (text != null) {
|
||||
final Event? localEvent =
|
||||
widget.messageEvent.getTextToSpeechLocal(langCode, text);
|
||||
if (localEvent != null) {
|
||||
localAudioEvent = localEvent;
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (text == null) {
|
||||
//TODO - handle error but get out of flow
|
||||
}
|
||||
|
||||
audioFile =
|
||||
await widget.messageEvent.getMatrixAudioFile(langCode, context);
|
||||
final Event? localEvent =
|
||||
widget.messageEvent.getTextToSpeechLocal(langCode, text!);
|
||||
|
||||
if (localEvent != null) {
|
||||
audioFile = await localEvent.getPangeaAudioFile();
|
||||
} else {
|
||||
audioFile = await widget.messageEvent.getMatrixAudioFile(
|
||||
langCode,
|
||||
context,
|
||||
);
|
||||
}
|
||||
debugPrint("audio file is now: $audioFile. setting starts and ends...");
|
||||
setSectionStartAndEndFromSelection();
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
debugPrint(StackTrace.current.toString());
|
||||
if (!mounted) return;
|
||||
setState(() => _isLoading = false);
|
||||
|
|
@ -68,19 +183,6 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
},
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
//once we have audio for words, we'll play that
|
||||
if (widget.overlayController.isSelection) {
|
||||
widget.overlayController.clearSelection();
|
||||
}
|
||||
|
||||
fetchAudio();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -91,15 +193,17 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
alignment: Alignment.center,
|
||||
child: _isLoading
|
||||
? const ToolbarContentLoadingIndicator()
|
||||
: localAudioEvent != null || audioFile != null
|
||||
: audioFile != null
|
||||
? Column(
|
||||
children: [
|
||||
AudioPlayerWidget(
|
||||
localAudioEvent,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
null,
|
||||
matrixFile: audioFile,
|
||||
autoplay: true,
|
||||
sectionStartMS: sectionStartMS,
|
||||
sectionEndMS: sectionEndMS,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
tts.missingVoiceButton,
|
||||
],
|
||||
)
|
||||
: const CardErrorWidget(),
|
||||
|
|
@ -109,6 +213,7 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
|
||||
class PangeaAudioFile extends MatrixAudioFile {
|
||||
List<int>? waveform;
|
||||
List<TTSToken> tokens;
|
||||
|
||||
PangeaAudioFile({
|
||||
required super.bytes,
|
||||
|
|
@ -116,5 +221,6 @@ class PangeaAudioFile extends MatrixAudioFile {
|
|||
super.mimeType,
|
||||
super.duration,
|
||||
this.waveform,
|
||||
required this.tokens,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:fluffychat/config/setting_keys.dart';
|
|||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/events/message_reactions.dart';
|
||||
import 'package:fluffychat/pangea/enum/activity_display_instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
|
|
@ -182,8 +183,10 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
void onClickOverlayMessageToken(
|
||||
PangeaToken token,
|
||||
) {
|
||||
if ([MessageMode.practiceActivity, MessageMode.textToSpeech]
|
||||
.contains(toolbarMode)) {
|
||||
if ([
|
||||
MessageMode.practiceActivity,
|
||||
// MessageMode.textToSpeech
|
||||
].contains(toolbarMode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -210,19 +213,23 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
|
||||
void setSelectedSpan(PracticeActivityModel activity) {
|
||||
final RelevantSpanDisplayDetails? span =
|
||||
activity.multipleChoice?.spanDisplayDetails;
|
||||
activity.content.spanDisplayDetails;
|
||||
|
||||
if (span == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedSpan = PangeaTokenText(
|
||||
offset: span.offset,
|
||||
length: span.length,
|
||||
content: widget._pangeaMessageEvent.messageDisplayText
|
||||
.substring(span.offset, span.offset + span.length),
|
||||
);
|
||||
if (span.displayInstructions != ActivityDisplayInstructionsEnum.nothing) {
|
||||
_selectedSpan = PangeaTokenText(
|
||||
offset: span.offset,
|
||||
length: span.length,
|
||||
content: widget._pangeaMessageEvent.messageDisplayText
|
||||
.substring(span.offset, span.offset + span.length),
|
||||
);
|
||||
} else {
|
||||
_selectedSpan = null;
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
|
@ -371,14 +378,17 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
widget.chatController.room.membership == Membership.join;
|
||||
|
||||
// the default spacing between the side of the screen and the message bubble
|
||||
final double messageMargin =
|
||||
pangeaMessageEvent.ownMessage ? Avatar.defaultSize + 16 : 8;
|
||||
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);
|
||||
final maxWidth = chatViewWidth - (2 * horizontalPadding) - messageMargin;
|
||||
const totalMaxWidth = (FluffyThemes.columnWidth * 2.5) - messageMargin;
|
||||
double maxWidth = chatViewWidth - (2 * horizontalPadding) - messageMargin;
|
||||
if (maxWidth > totalMaxWidth) {
|
||||
maxWidth = totalMaxWidth;
|
||||
}
|
||||
|
||||
final overlayMessage = Container(
|
||||
constraints: BoxConstraints(
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ class MessageToolbar extends StatelessWidget {
|
|||
return MessageAudioCard(
|
||||
messageEvent: pangeaMessageEvent,
|
||||
overlayController: overLayController,
|
||||
selection: overLayController.selectedSpan,
|
||||
);
|
||||
case MessageMode.speechToText:
|
||||
return MessageSpeechToTextCard(
|
||||
|
|
|
|||
|
|
@ -70,34 +70,32 @@ class ToolbarButtons extends StatelessWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: modes
|
||||
.mapIndexed(
|
||||
(index, mode) => Tooltip(
|
||||
message: mode.tooltip(context),
|
||||
child: IconButton(
|
||||
iconSize: 20,
|
||||
icon: Icon(mode.icon),
|
||||
color: mode == overlayController.toolbarMode
|
||||
? Colors.white
|
||||
: null,
|
||||
isSelected: mode == overlayController.toolbarMode,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
mode.iconButtonColor(
|
||||
context,
|
||||
index,
|
||||
overlayController.toolbarMode,
|
||||
pangeaMessageEvent.numberOfActivitiesCompleted,
|
||||
overlayController.isPracticeComplete,
|
||||
),
|
||||
(index, mode) => IconButton(
|
||||
iconSize: 20,
|
||||
icon: Icon(mode.icon),
|
||||
tooltip: mode.tooltip(context),
|
||||
color: mode == overlayController.toolbarMode
|
||||
? Colors.white
|
||||
: null,
|
||||
isSelected: mode == overlayController.toolbarMode,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
mode.iconButtonColor(
|
||||
context,
|
||||
index,
|
||||
overlayController.toolbarMode,
|
||||
pangeaMessageEvent.numberOfActivitiesCompleted,
|
||||
overlayController.isPracticeComplete,
|
||||
),
|
||||
),
|
||||
onPressed: mode.isUnlocked(
|
||||
index,
|
||||
pangeaMessageEvent.numberOfActivitiesCompleted,
|
||||
overlayController.isPracticeComplete,
|
||||
)
|
||||
? () => overlayController.updateToolbarMode(mode)
|
||||
: null,
|
||||
),
|
||||
onPressed: mode.isUnlocked(
|
||||
index,
|
||||
pangeaMessageEvent.numberOfActivitiesCompleted,
|
||||
overlayController.isPracticeComplete,
|
||||
)
|
||||
? () => overlayController.updateToolbarMode(mode)
|
||||
: null,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
|
|
|
|||
61
lib/pangea/widgets/chat/missing_voice_button.dart
Normal file
61
lib/pangea/widgets/chat/missing_voice_button.dart
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:android_intent_plus/android_intent.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
|
||||
class MissingVoiceButton extends StatelessWidget {
|
||||
final String targetLangCode;
|
||||
|
||||
const MissingVoiceButton({
|
||||
required this.targetLangCode,
|
||||
super.key,
|
||||
});
|
||||
|
||||
void launchTTSSettings(BuildContext context) {
|
||||
if (Platform.isAndroid) {
|
||||
const intent = AndroidIntent(
|
||||
action: 'com.android.settings.TTS_SETTINGS',
|
||||
package: 'com.talktolearn.chat',
|
||||
);
|
||||
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: intent.launch,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context)!.voiceNotAvailable,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => launchTTSSettings,
|
||||
style: const ButtonStyle(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: Text(L10n.of(context)!.openVoiceSettings),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
123
lib/pangea/widgets/chat/tts_controller.dart
Normal file
123
lib/pangea/widgets/chat/tts_controller.dart
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/missing_voice_button.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_tts/flutter_tts.dart' as flutter_tts;
|
||||
|
||||
class TtsController {
|
||||
String? targetLanguage;
|
||||
|
||||
List<String> availableLangCodes = [];
|
||||
final flutter_tts.FlutterTts tts = flutter_tts.FlutterTts();
|
||||
|
||||
TtsController() {
|
||||
setupTTS();
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
await tts.stop();
|
||||
}
|
||||
|
||||
onError(dynamic message) => ErrorHandler.logError(
|
||||
m: 'TTS error',
|
||||
data: {
|
||||
'message': message,
|
||||
},
|
||||
);
|
||||
|
||||
Future<void> setupTTS() async {
|
||||
try {
|
||||
tts.setErrorHandler(onError);
|
||||
|
||||
targetLanguage ??=
|
||||
MatrixState.pangeaController.languageController.userL2?.langCode;
|
||||
|
||||
debugger(when: kDebugMode && targetLanguage == null);
|
||||
|
||||
tts.setLanguage(
|
||||
targetLanguage ?? "en",
|
||||
);
|
||||
|
||||
await tts.awaitSpeakCompletion(true);
|
||||
|
||||
final voices = await tts.getVoices;
|
||||
availableLangCodes = (voices as List)
|
||||
.map((v) {
|
||||
// on iOS / web, the codes are in 'locale', but on Android, they are in 'name'
|
||||
final nameCode = v['name']?.split("-").first;
|
||||
final localeCode = v['locale']?.split("-").first;
|
||||
return nameCode.length == 2 ? nameCode : localeCode;
|
||||
})
|
||||
.toSet()
|
||||
.cast<String>()
|
||||
.toList();
|
||||
|
||||
debugPrint("availableLangCodes: $availableLangCodes");
|
||||
|
||||
debugger(when: kDebugMode && !isLanguageFullySupported);
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: s);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
try {
|
||||
// return type is dynamic but apparent its supposed to be 1
|
||||
// https://pub.dev/packages/flutter_tts
|
||||
final result = await tts.stop();
|
||||
if (result != 1) {
|
||||
ErrorHandler.logError(
|
||||
m: 'Unexpected result from tts.stop',
|
||||
data: {
|
||||
'result': result,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
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;
|
||||
|
||||
final result = await tts.speak(text);
|
||||
|
||||
// return type is dynamic but apparent its supposed to be 1
|
||||
// https://pub.dev/packages/flutter_tts
|
||||
if (result != 1) {
|
||||
ErrorHandler.logError(
|
||||
m: 'Unexpected result from tts.speak',
|
||||
data: {
|
||||
'result': result,
|
||||
'text': text,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: s);
|
||||
}
|
||||
}
|
||||
|
||||
bool get isLanguageFullySupported =>
|
||||
availableLangCodes.contains(targetLanguage);
|
||||
|
||||
Widget get missingVoiceButton => targetLanguage != null &&
|
||||
(kIsWeb || isLanguageFullySupported || !PlatformInfos.isAndroid)
|
||||
? const SizedBox.shrink()
|
||||
: MissingVoiceButton(
|
||||
targetLangCode: targetLanguage!,
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/get_grammar_copy.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
|
|
@ -47,7 +49,13 @@ class AnalyticsPopup extends StatelessWidget {
|
|||
child: ListTile(
|
||||
onTap: () {},
|
||||
title: Text(
|
||||
constructsModel.constructList[index].lemma,
|
||||
constructsModel.type == ConstructTypeEnum.morph
|
||||
? getGrammarCopy(
|
||||
constructsModel
|
||||
.constructList[index].lemma,
|
||||
context,
|
||||
)
|
||||
: constructsModel.constructList[index].lemma,
|
||||
),
|
||||
subtitle: LinearProgressIndicator(
|
||||
value:
|
||||
|
|
|
|||
|
|
@ -1,268 +0,0 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../../widgets/matrix.dart';
|
||||
import '../../utils/firebase_analytics.dart';
|
||||
|
||||
//PTODO - auto invite students when you add a space and delete the add_class_and_invite.dart file
|
||||
class AddToSpaceToggles extends StatefulWidget {
|
||||
final String? roomId;
|
||||
final bool startOpen;
|
||||
final String? activeSpaceId;
|
||||
final bool spaceMode;
|
||||
|
||||
const AddToSpaceToggles({
|
||||
super.key,
|
||||
this.roomId,
|
||||
this.startOpen = false,
|
||||
this.activeSpaceId,
|
||||
this.spaceMode = false,
|
||||
});
|
||||
|
||||
@override
|
||||
AddToSpaceState createState() => AddToSpaceState();
|
||||
}
|
||||
|
||||
class AddToSpaceState extends State<AddToSpaceToggles> {
|
||||
late Room? room;
|
||||
late Room? parent;
|
||||
late List<Room> possibleParents;
|
||||
late bool isOpen;
|
||||
late bool isSuggested;
|
||||
|
||||
AddToSpaceState({Key? key});
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
initialize();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(AddToSpaceToggles oldWidget) {
|
||||
if (oldWidget.roomId != widget.roomId) {
|
||||
initialize();
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
void initialize() {
|
||||
//if roomId is null, it means this widget is being used in the creation flow
|
||||
room = widget.roomId != null
|
||||
? Matrix.of(context).client.getRoomById(widget.roomId!)
|
||||
: null;
|
||||
|
||||
isSuggested = true;
|
||||
room?.isSuggested().then((value) => isSuggested = value);
|
||||
|
||||
possibleParents = Matrix.of(context)
|
||||
.client
|
||||
.rooms
|
||||
.where(
|
||||
(Room r) => r.isSpace && widget.roomId != r.id,
|
||||
)
|
||||
.toList();
|
||||
|
||||
parent = widget.roomId != null
|
||||
? possibleParents.firstWhereOrNull(
|
||||
(r) => r.spaceChildren.any((room) => room.roomId == widget.roomId),
|
||||
)
|
||||
: null;
|
||||
|
||||
//sort possibleParents
|
||||
//if possibleParent in parents, put first
|
||||
//use sort but use any instead of contains because contains uses == and we want to compare by id
|
||||
possibleParents.sort((a, b) {
|
||||
if (parent?.id == a.id) {
|
||||
return -1;
|
||||
} else if (parent?.id == b.id) {
|
||||
return 1;
|
||||
} else {
|
||||
return a.name.compareTo(b.name);
|
||||
}
|
||||
});
|
||||
|
||||
isOpen = widget.startOpen;
|
||||
|
||||
if (widget.activeSpaceId != null) {
|
||||
final activeSpace =
|
||||
Matrix.of(context).client.getRoomById(widget.activeSpaceId!);
|
||||
if (activeSpace == null) {
|
||||
ErrorHandler.logError(
|
||||
e: Exception('activeSpaceId ${widget.activeSpaceId} not found'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (activeSpace.canSendEvent(EventTypes.SpaceChild)) {
|
||||
parent = activeSpace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _addSingleSpace(String roomToAddId, Room newParent) async {
|
||||
GoogleAnalytics.addParent(roomToAddId, newParent.classCode);
|
||||
await newParent.pangeaSetSpaceChild(
|
||||
roomToAddId,
|
||||
suggested: isSuggested,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> addSpaces(String roomToAddId) async {
|
||||
if (parent == null) return;
|
||||
await _addSingleSpace(roomToAddId, parent!);
|
||||
}
|
||||
|
||||
Future<void> handleAdd(bool add, Room possibleParent) async {
|
||||
//in this case, the room has already been made so we handle adding as it happens
|
||||
if (room != null) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => add
|
||||
? _addSingleSpace(room!.id, possibleParent)
|
||||
: possibleParent.removeSpaceChild(room!.id),
|
||||
onError: (e) {
|
||||
// if error occurs, do not change value of toggle
|
||||
add = !add;
|
||||
return (e as Object?)?.toLocalizedString(context) ??
|
||||
e?.toString() ??
|
||||
L10n.of(context)!.oopsSomethingWentWrong;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setState(
|
||||
() => add ? parent = possibleParent : parent = null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget getAddToSpaceToggleItem(int index) {
|
||||
final Room possibleParent = possibleParents[index];
|
||||
final bool canAdd = possibleParent.canAddAsParentOf(
|
||||
room,
|
||||
spaceMode: widget.spaceMode,
|
||||
);
|
||||
|
||||
return Opacity(
|
||||
opacity: canAdd ? 1 : 0.5,
|
||||
child: Column(
|
||||
children: [
|
||||
SwitchListTile.adaptive(
|
||||
title: possibleParent.nameAndRoomTypeIcon(),
|
||||
activeColor: AppConfig.activeToggleColor,
|
||||
value: parent?.id == possibleParent.id,
|
||||
onChanged: (bool add) => canAdd
|
||||
? handleAdd(add, possibleParent)
|
||||
: ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.noPermission),
|
||||
),
|
||||
),
|
||||
),
|
||||
Divider(
|
||||
height: 0.5,
|
||||
color: Theme.of(context).colorScheme.secondary.withAlpha(25),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setSuggested(bool suggested) async {
|
||||
setState(() => isSuggested = suggested);
|
||||
if (room != null) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async => await room?.setSuggested(suggested),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.addToSpace,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
widget.spaceMode || (room?.isSpace ?? false)
|
||||
? L10n.of(context)!.addSpaceToSpaceDesc
|
||||
: L10n.of(context)!.addChatToSpaceDesc,
|
||||
),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
child: const Icon(Icons.workspaces_outlined),
|
||||
),
|
||||
trailing: Icon(
|
||||
isOpen
|
||||
? Icons.keyboard_arrow_down_outlined
|
||||
: Icons.keyboard_arrow_right_outlined,
|
||||
),
|
||||
onTap: () {
|
||||
setState(() => isOpen = !isOpen);
|
||||
},
|
||||
),
|
||||
if (isOpen) ...[
|
||||
const Divider(height: 1),
|
||||
possibleParents.isNotEmpty
|
||||
? Column(
|
||||
children: [
|
||||
SwitchListTile.adaptive(
|
||||
title: Text(
|
||||
widget.spaceMode || (room?.isSpace ?? false)
|
||||
? L10n.of(context)!.suggestToSpace
|
||||
: L10n.of(context)!.suggestToChat,
|
||||
),
|
||||
secondary: Icon(
|
||||
isSuggested
|
||||
? Icons.visibility_outlined
|
||||
: Icons.visibility_off_outlined,
|
||||
),
|
||||
subtitle: Text(
|
||||
widget.spaceMode || (room?.isSpace ?? false)
|
||||
? L10n.of(context)!.suggestToSpaceDesc
|
||||
: L10n.of(context)!.suggestToChatDesc,
|
||||
),
|
||||
activeColor: AppConfig.activeToggleColor,
|
||||
value: isSuggested,
|
||||
onChanged: (bool add) => setSuggested(add),
|
||||
),
|
||||
Divider(
|
||||
height: 0.5,
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondary.withAlpha(25),
|
||||
),
|
||||
...possibleParents.mapIndexed(
|
||||
(index, _) => getAddToSpaceToggleItem(index),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
L10n.of(context)!.inNoSpaces,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
import 'package:fluffychat/pangea/constants/local.key.dart';
|
||||
import 'package:fluffychat/pangea/constants/url_query_parameter_keys.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../../../widgets/matrix.dart';
|
||||
import '../../constants/local.key.dart';
|
||||
|
||||
//if on home with classcode in url and not logged in, then save it soemhow and after llogin, join class automatically
|
||||
//if on home with classcode in url and logged in, then join class automatically
|
||||
class JoinClassWithLink extends StatefulWidget {
|
||||
|
|
@ -19,7 +18,7 @@ class JoinClassWithLink extends StatefulWidget {
|
|||
//PTODO - show class info in field so they know they're joining the right class
|
||||
class _JoinClassWithLinkState extends State<JoinClassWithLink> {
|
||||
String? classCode;
|
||||
final PangeaController _pangeaController = MatrixState.pangeaController;
|
||||
final PangeaController pangeaController = MatrixState.pangeaController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -39,8 +38,7 @@ class _JoinClassWithLinkState extends State<JoinClassWithLink> {
|
|||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await _pangeaController.pStoreService.save(
|
||||
await pangeaController.pStoreService.save(
|
||||
PLocalKey.cachedClassCodeToJoin,
|
||||
classCode,
|
||||
isAccountData: false,
|
||||
|
|
|
|||
|
|
@ -36,21 +36,20 @@ class ContentIssueButton extends StatelessWidget {
|
|||
L10n.of(context)!.reportContentIssueTitle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
content: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 300),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const BotFace(
|
||||
width: 60,
|
||||
expression: BotExpression.addled,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(L10n.of(context)!.reportContentIssueDescription),
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextField(
|
||||
content: SingleChildScrollView(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 300),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const BotFace(
|
||||
width: 60,
|
||||
expression: BotExpression.addled,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(L10n.of(context)!.reportContentIssueDescription),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: feedbackController,
|
||||
decoration: InputDecoration(
|
||||
labelText: L10n.of(context)!.feedback,
|
||||
|
|
@ -58,8 +57,8 @@ class ContentIssueButton extends StatelessWidget {
|
|||
),
|
||||
maxLines: 4,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
|
|
|
|||
|
|
@ -1,95 +0,0 @@
|
|||
import 'package:fluffychat/pangea/models/bot_options_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class ConversationBotCustomSystemPromptInput extends StatelessWidget {
|
||||
final BotOptionsModel initialBotOptions;
|
||||
// call this to update propagate changes to parents
|
||||
final void Function(BotOptionsModel) onChanged;
|
||||
|
||||
const ConversationBotCustomSystemPromptInput({
|
||||
super.key,
|
||||
required this.initialBotOptions,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String customSystemPrompt = initialBotOptions.customSystemPrompt ?? "";
|
||||
|
||||
final TextEditingController textFieldController =
|
||||
TextEditingController(text: customSystemPrompt);
|
||||
|
||||
final GlobalKey<FormState> customSystemPromptFormKey =
|
||||
GlobalKey<FormState>();
|
||||
|
||||
void setBotCustomSystemPromptAction() async {
|
||||
showDialog(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: Text(
|
||||
L10n.of(context)!.conversationBotCustomZone_customSystemPromptLabel,
|
||||
),
|
||||
content: Form(
|
||||
key: customSystemPromptFormKey,
|
||||
child: TextFormField(
|
||||
minLines: 1,
|
||||
maxLines: 10,
|
||||
maxLength: 1000,
|
||||
controller: textFieldController,
|
||||
onChanged: (value) {
|
||||
if (value.isNotEmpty) {
|
||||
customSystemPrompt = value;
|
||||
}
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'This field cannot be empty';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(L10n.of(context)!.cancel),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(L10n.of(context)!.ok),
|
||||
onPressed: () {
|
||||
if (customSystemPromptFormKey.currentState!.validate()) {
|
||||
if (customSystemPrompt !=
|
||||
initialBotOptions.customSystemPrompt) {
|
||||
initialBotOptions.customSystemPrompt = customSystemPrompt;
|
||||
onChanged.call(initialBotOptions);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
onTap: setBotCustomSystemPromptAction,
|
||||
title: Text(
|
||||
initialBotOptions.customSystemPrompt ??
|
||||
L10n.of(context)!
|
||||
.conversationBotCustomZone_customSystemPromptPlaceholder,
|
||||
),
|
||||
subtitle: customSystemPrompt.isEmpty
|
||||
? Text(
|
||||
L10n.of(context)!
|
||||
.conversationBotCustomZone_customSystemPromptEmptyError,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import 'package:fluffychat/pangea/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_custom_system_prompt_input.dart';
|
||||
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_label.dart';
|
||||
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_title.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class ConversationBotCustomZone extends StatelessWidget {
|
||||
final BotOptionsModel initialBotOptions;
|
||||
// call this to update propagate changes to parents
|
||||
final void Function(BotOptionsModel) onChanged;
|
||||
|
||||
const ConversationBotCustomZone({
|
||||
super.key,
|
||||
required this.initialBotOptions,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
ConversationBotDynamicZoneTitle(
|
||||
title: L10n.of(context)!.conversationBotCustomZone_title,
|
||||
),
|
||||
ConversationBotDynamicZoneLabel(
|
||||
label: L10n.of(context)!
|
||||
.conversationBotCustomZone_customSystemPromptLabel,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: ConversationBotCustomSystemPromptInput(
|
||||
initialBotOptions: initialBotOptions,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
CheckboxListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!
|
||||
.conversationBotCustomZone_customTriggerReactionEnabledLabel,
|
||||
),
|
||||
enabled: false,
|
||||
value: initialBotOptions.customTriggerReactionEnabled ?? true,
|
||||
onChanged: (value) {
|
||||
initialBotOptions.customTriggerReactionEnabled = value ?? true;
|
||||
initialBotOptions.customTriggerReactionKey =
|
||||
"⏩"; // hard code this for now
|
||||
onChanged.call(initialBotOptions);
|
||||
},
|
||||
// make this input disabled always
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
import 'package:fluffychat/pangea/models/bot_options_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class ConversationBotDiscussionKeywordsInput extends StatelessWidget {
|
||||
final BotOptionsModel initialBotOptions;
|
||||
// call this to update propagate changes to parents
|
||||
final void Function(BotOptionsModel) onChanged;
|
||||
|
||||
const ConversationBotDiscussionKeywordsInput({
|
||||
super.key,
|
||||
required this.initialBotOptions,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String discussionKeywords = initialBotOptions.discussionKeywords ?? "";
|
||||
|
||||
final TextEditingController textFieldController =
|
||||
TextEditingController(text: discussionKeywords);
|
||||
|
||||
void setBotDiscussionKeywordsAction() async {
|
||||
showDialog(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: Text(
|
||||
L10n.of(context)!
|
||||
.conversationBotDiscussionZone_discussionKeywordsLabel,
|
||||
),
|
||||
content: TextField(
|
||||
minLines: 1,
|
||||
maxLines: 10,
|
||||
maxLength: 1000,
|
||||
controller: textFieldController,
|
||||
onChanged: (value) {
|
||||
discussionKeywords = value;
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(L10n.of(context)!.cancel),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(L10n.of(context)!.ok),
|
||||
onPressed: () {
|
||||
if (discussionKeywords == "") return;
|
||||
if (discussionKeywords !=
|
||||
initialBotOptions.discussionKeywords) {
|
||||
initialBotOptions.discussionKeywords = discussionKeywords;
|
||||
onChanged.call(initialBotOptions);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
onTap: setBotDiscussionKeywordsAction,
|
||||
title: Text(
|
||||
initialBotOptions.discussionKeywords ??
|
||||
L10n.of(context)!
|
||||
.conversationBotDiscussionZone_discussionKeywordsPlaceholder,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
import 'package:fluffychat/pangea/models/bot_options_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class ConversationBotDiscussionTopicInput extends StatelessWidget {
|
||||
final BotOptionsModel initialBotOptions;
|
||||
// call this to update propagate changes to parents
|
||||
final void Function(BotOptionsModel) onChanged;
|
||||
|
||||
const ConversationBotDiscussionTopicInput({
|
||||
super.key,
|
||||
required this.initialBotOptions,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String discussionTopic = initialBotOptions.discussionTopic ?? "";
|
||||
|
||||
final TextEditingController textFieldController =
|
||||
TextEditingController(text: discussionTopic);
|
||||
|
||||
void setBotDiscussionTopicAction() async {
|
||||
showDialog(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: Text(
|
||||
L10n.of(context)!
|
||||
.conversationBotDiscussionZone_discussionTopicLabel,
|
||||
),
|
||||
content: TextField(
|
||||
minLines: 1,
|
||||
maxLines: 10,
|
||||
maxLength: 1000,
|
||||
controller: textFieldController,
|
||||
onChanged: (value) {
|
||||
discussionTopic = value;
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(L10n.of(context)!.cancel),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(L10n.of(context)!.ok),
|
||||
onPressed: () {
|
||||
if (discussionTopic == "") return;
|
||||
if (discussionTopic != initialBotOptions.discussionTopic) {
|
||||
initialBotOptions.discussionTopic = discussionTopic;
|
||||
onChanged.call(initialBotOptions);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
onTap: setBotDiscussionTopicAction,
|
||||
title: Text(
|
||||
initialBotOptions.discussionTopic ??
|
||||
L10n.of(context)!
|
||||
.conversationBotDiscussionZone_discussionTopicPlaceholder,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
import 'package:fluffychat/pangea/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_discussion_keywords_input.dart';
|
||||
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_discussion_topic_input.dart';
|
||||
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_label.dart';
|
||||
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_title.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class ConversationBotDiscussionZone extends StatelessWidget {
|
||||
final BotOptionsModel initialBotOptions;
|
||||
// call this to update propagate changes to parents
|
||||
final void Function(BotOptionsModel) onChanged;
|
||||
|
||||
const ConversationBotDiscussionZone({
|
||||
super.key,
|
||||
required this.initialBotOptions,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
ConversationBotDynamicZoneTitle(
|
||||
title: L10n.of(context)!.conversationBotDiscussionZone_title,
|
||||
),
|
||||
ConversationBotDynamicZoneLabel(
|
||||
label: L10n.of(context)!
|
||||
.conversationBotDiscussionZone_discussionTopicLabel,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: ConversationBotDiscussionTopicInput(
|
||||
initialBotOptions: initialBotOptions,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ConversationBotDynamicZoneLabel(
|
||||
label: L10n.of(context)!
|
||||
.conversationBotDiscussionZone_discussionKeywordsLabel,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: ConversationBotDiscussionKeywordsInput(
|
||||
initialBotOptions: initialBotOptions,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
CheckboxListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!
|
||||
.conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel,
|
||||
),
|
||||
enabled: false,
|
||||
value: initialBotOptions.discussionTriggerReactionEnabled ?? true,
|
||||
onChanged: (value) {
|
||||
initialBotOptions.discussionTriggerReactionEnabled = value ?? true;
|
||||
initialBotOptions.discussionTriggerReactionKey =
|
||||
"⏩"; // hard code this for now
|
||||
onChanged.call(initialBotOptions);
|
||||
},
|
||||
// make this input disabled always
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class ConversationBotDynamicZoneLabel extends StatelessWidget {
|
||||
final String label;
|
||||
|
||||
const ConversationBotDynamicZoneLabel({
|
||||
super.key,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 0, 0),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class ConversationBotDynamicZoneTitle extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
const ConversationBotDynamicZoneTitle({
|
||||
super.key,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
color: Colors.grey,
|
||||
thickness: 1,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +1,93 @@
|
|||
import 'package:fluffychat/pangea/constants/bot_mode.dart';
|
||||
import 'package:fluffychat/pangea/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_custom_zone.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'conversation_bot_discussion_zone.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class ConversationBotModeDynamicZone extends StatelessWidget {
|
||||
final BotOptionsModel initialBotOptions;
|
||||
final void Function(BotOptionsModel) onChanged;
|
||||
final BotOptionsModel botOptions;
|
||||
final TextEditingController discussionTopicController;
|
||||
final TextEditingController discussionKeywordsController;
|
||||
final TextEditingController customSystemPromptController;
|
||||
|
||||
final bool enabled;
|
||||
|
||||
const ConversationBotModeDynamicZone({
|
||||
super.key,
|
||||
required this.initialBotOptions,
|
||||
required this.onChanged,
|
||||
required this.botOptions,
|
||||
required this.discussionTopicController,
|
||||
required this.discussionKeywordsController,
|
||||
required this.customSystemPromptController,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final zoneMap = {
|
||||
BotMode.discussion: ConversationBotDiscussionZone(
|
||||
initialBotOptions: initialBotOptions,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
BotMode.custom: ConversationBotCustomZone(
|
||||
initialBotOptions: initialBotOptions,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
};
|
||||
if (!zoneMap.containsKey(initialBotOptions.mode)) {
|
||||
return Container();
|
||||
}
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
width: 0.5,
|
||||
final discussionChildren = [
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.of(context)!
|
||||
.conversationBotDiscussionZone_discussionTopicPlaceholder,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
controller: discussionTopicController,
|
||||
validator: (value) => enabled &&
|
||||
botOptions.mode == BotMode.discussion &&
|
||||
(value == null || value.isEmpty)
|
||||
? L10n.of(context)!.enterDiscussionTopic
|
||||
: null,
|
||||
enabled: enabled,
|
||||
minLines: 1, // Minimum number of lines
|
||||
maxLines: null, // Allow the field to expand based on content
|
||||
keyboardType: TextInputType.multiline,
|
||||
),
|
||||
child: zoneMap[initialBotOptions.mode],
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.of(context)!
|
||||
.conversationBotDiscussionZone_discussionKeywordsPlaceholder,
|
||||
),
|
||||
controller: discussionKeywordsController,
|
||||
enabled: enabled,
|
||||
minLines: 1, // Minimum number of lines
|
||||
maxLines: null, // Allow the field to expand based on content
|
||||
keyboardType: TextInputType.multiline,
|
||||
),
|
||||
];
|
||||
|
||||
final customChildren = [
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.of(context)!
|
||||
.conversationBotCustomZone_customSystemPromptPlaceholder,
|
||||
),
|
||||
validator: (value) => enabled &&
|
||||
botOptions.mode == BotMode.custom &&
|
||||
(value == null || value.isEmpty)
|
||||
? L10n.of(context)!.enterPrompt
|
||||
: null,
|
||||
controller: customSystemPromptController,
|
||||
enabled: enabled,
|
||||
minLines: 1, // Minimum number of lines
|
||||
maxLines: null, // Allow the field to expand based on content
|
||||
keyboardType: TextInputType.multiline,
|
||||
),
|
||||
];
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (botOptions.mode == BotMode.discussion) ...discussionChildren,
|
||||
if (botOptions.mode == BotMode.custom) ...customChildren,
|
||||
const SizedBox(height: 12),
|
||||
CheckboxListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!
|
||||
.conversationBotCustomZone_customTriggerReactionEnabledLabel,
|
||||
),
|
||||
enabled: false,
|
||||
value: botOptions.customTriggerReactionEnabled ?? true,
|
||||
onChanged: null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|||
|
||||
class ConversationBotModeSelect extends StatelessWidget {
|
||||
final String? initialMode;
|
||||
final void Function(String?)? onChanged;
|
||||
final void Function(String?) onChanged;
|
||||
final bool enabled;
|
||||
|
||||
const ConversationBotModeSelect({
|
||||
super.key,
|
||||
this.initialMode,
|
||||
this.onChanged,
|
||||
required this.onChanged,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -24,56 +26,35 @@ class ConversationBotModeSelect extends StatelessWidget {
|
|||
// L10n.of(context)!.conversationBotModeSelectOption_storyGame,
|
||||
};
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
width: 0.5,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
child: DropdownButton(
|
||||
// Initial Value
|
||||
hint: Padding(
|
||||
padding: const EdgeInsets.only(left: 15),
|
||||
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
|
||||
items: [
|
||||
for (final entry in options.entries)
|
||||
DropdownMenuItem(
|
||||
value: entry.key,
|
||||
child: Text(
|
||||
options[initialMode ?? BotMode.discussion]!,
|
||||
style: const TextStyle().copyWith(
|
||||
color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
fontSize: 14,
|
||||
),
|
||||
entry.value,
|
||||
overflow: TextOverflow.clip,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
isExpanded: true,
|
||||
underline: Container(),
|
||||
// Down Arrow Icon
|
||||
icon: const Icon(Icons.keyboard_arrow_down),
|
||||
// Array list of items
|
||||
items: [
|
||||
for (final entry in options.entries)
|
||||
DropdownMenuItem(
|
||||
value: entry.key,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 15),
|
||||
child: Text(
|
||||
entry.value,
|
||||
style: const TextStyle().copyWith(
|
||||
color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
fontSize: 14,
|
||||
),
|
||||
overflow: TextOverflow.clip,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: enabled ? onChanged : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/constants/bot_mode.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/bot_options_model.dart';
|
||||
|
|
@ -11,19 +13,14 @@ import 'package:fluffychat/widgets/matrix.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class ConversationBotSettings extends StatefulWidget {
|
||||
final Room? room;
|
||||
final bool startOpen;
|
||||
final String? activeSpaceId;
|
||||
final Room room;
|
||||
|
||||
const ConversationBotSettings({
|
||||
super.key,
|
||||
this.room,
|
||||
this.startOpen = false,
|
||||
this.activeSpaceId,
|
||||
required this.room,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -31,37 +28,10 @@ class ConversationBotSettings extends StatefulWidget {
|
|||
}
|
||||
|
||||
class ConversationBotSettingsState extends State<ConversationBotSettings> {
|
||||
late BotOptionsModel botOptions;
|
||||
late bool isOpen;
|
||||
late bool isCreating;
|
||||
bool addBot = false;
|
||||
Room? parentSpace;
|
||||
|
||||
ConversationBotSettingsState({Key? key});
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
isOpen = widget.startOpen;
|
||||
botOptions = widget.room?.botOptions != null
|
||||
? BotOptionsModel.fromJson(widget.room?.botOptions?.toJson())
|
||||
: BotOptionsModel();
|
||||
widget.room?.botIsInRoom.then((bool isBotRoom) {
|
||||
setState(() {
|
||||
addBot = isBotRoom;
|
||||
});
|
||||
});
|
||||
parentSpace = widget.activeSpaceId != null
|
||||
? Matrix.of(context).client.getRoomById(widget.activeSpaceId!)
|
||||
: null;
|
||||
isCreating = widget.room == null;
|
||||
}
|
||||
|
||||
Future<void> setBotOption() async {
|
||||
if (widget.room == null) return;
|
||||
Future<void> setBotOptions(BotOptionsModel botOptions) async {
|
||||
try {
|
||||
await Matrix.of(context).client.setRoomStateWithKey(
|
||||
widget.room!.id,
|
||||
widget.room.id,
|
||||
PangeaEventTypes.botOptions,
|
||||
'',
|
||||
botOptions.toJson(),
|
||||
|
|
@ -72,20 +42,16 @@ class ConversationBotSettingsState extends State<ConversationBotSettings> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> updateBotOption(void Function() makeLocalChange) async {
|
||||
makeLocalChange();
|
||||
await showFutureLoadingDialog(
|
||||
Future<void> showBotOptionsDialog() async {
|
||||
final BotOptionsModel? newBotOptions = await showDialog<BotOptionsModel?>(
|
||||
context: context,
|
||||
future: () async {
|
||||
try {
|
||||
await setBotOption();
|
||||
} catch (err, stack) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: stack);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
builder: (BuildContext context) =>
|
||||
ConversationBotSettingsDialog(room: widget.room),
|
||||
);
|
||||
|
||||
if (newBotOptions != null) {
|
||||
setBotOptions(newBotOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -98,17 +64,12 @@ class ConversationBotSettingsState extends State<ConversationBotSettings> {
|
|||
children: [
|
||||
ListTile(
|
||||
title: Text(
|
||||
isCreating
|
||||
? L10n.of(context)!.addConversationBot
|
||||
: L10n.of(context)!.botConfig,
|
||||
L10n.of(context)!.botConfig,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
subtitle: isCreating
|
||||
? Text(L10n.of(context)!.addConversationBotDesc)
|
||||
: null,
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
|
|
@ -117,164 +78,191 @@ class ConversationBotSettingsState extends State<ConversationBotSettings> {
|
|||
expression: BotExpression.idle,
|
||||
),
|
||||
),
|
||||
trailing: isCreating
|
||||
? ElevatedButton(
|
||||
onPressed: () async {
|
||||
final bool? confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: addBot
|
||||
? Text(
|
||||
L10n.of(context)!
|
||||
.addConversationBotButtonTitleRemove,
|
||||
)
|
||||
: Text(
|
||||
L10n.of(context)!
|
||||
.addConversationBotDialogTitleInvite,
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Text(L10n.of(context)!.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(!addBot);
|
||||
},
|
||||
child: addBot
|
||||
? Text(
|
||||
L10n.of(context)!
|
||||
.addConversationBotDialogRemoveConfirmation,
|
||||
)
|
||||
: Text(
|
||||
L10n.of(context)!
|
||||
.addConversationBotDialogInviteConfirmation,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (confirm == true) {
|
||||
setState(() => addBot = true);
|
||||
widget.room?.invite(BotName.byEnvironment);
|
||||
} else {
|
||||
setState(() => addBot = false);
|
||||
widget.room?.kick(BotName.byEnvironment);
|
||||
}
|
||||
},
|
||||
child: addBot
|
||||
? Text(
|
||||
L10n.of(context)!.addConversationBotButtonRemove,
|
||||
)
|
||||
: Text(
|
||||
L10n.of(context)!.addConversationBotButtonInvite,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.settings),
|
||||
onTap: isCreating
|
||||
? null
|
||||
: () async {
|
||||
final bool? confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
title: Text(
|
||||
L10n.of(context)!.botConfig,
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.fromLTRB(0, 0, 0, 12),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context)!.conversationBotStatus,
|
||||
),
|
||||
Switch(
|
||||
value: addBot,
|
||||
onChanged: (value) {
|
||||
setState(
|
||||
() => addBot = value,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (addBot)
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary,
|
||||
width: 0.5,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(10),
|
||||
),
|
||||
),
|
||||
child: ConversationBotSettingsForm(
|
||||
botOptions: botOptions,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Text(L10n.of(context)!.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
child: Text(
|
||||
L10n.of(context)!
|
||||
.conversationBotConfigConfirmChange,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
if (confirm == true) {
|
||||
updateBotOption(() {
|
||||
botOptions = botOptions;
|
||||
});
|
||||
final bool isBotRoomMember =
|
||||
await widget.room?.botIsInRoom ?? false;
|
||||
if (addBot && !isBotRoomMember) {
|
||||
await widget.room?.invite(BotName.byEnvironment);
|
||||
} else if (!addBot && isBotRoomMember) {
|
||||
await widget.room?.kick(BotName.byEnvironment);
|
||||
}
|
||||
}
|
||||
},
|
||||
trailing: const Icon(Icons.settings),
|
||||
onTap: showBotOptionsDialog,
|
||||
),
|
||||
if (isCreating && addBot)
|
||||
ConversationBotSettingsForm(
|
||||
botOptions: botOptions,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConversationBotSettingsDialog extends StatefulWidget {
|
||||
final Room room;
|
||||
|
||||
const ConversationBotSettingsDialog({
|
||||
super.key,
|
||||
required this.room,
|
||||
});
|
||||
|
||||
@override
|
||||
ConversationBotSettingsDialogState createState() =>
|
||||
ConversationBotSettingsDialogState();
|
||||
}
|
||||
|
||||
class ConversationBotSettingsDialogState
|
||||
extends State<ConversationBotSettingsDialog> {
|
||||
late BotOptionsModel botOptions;
|
||||
bool addBot = false;
|
||||
|
||||
final TextEditingController discussionTopicController =
|
||||
TextEditingController();
|
||||
final TextEditingController discussionKeywordsController =
|
||||
TextEditingController();
|
||||
final TextEditingController customSystemPromptController =
|
||||
TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
botOptions = widget.room.botOptions != null
|
||||
? BotOptionsModel.fromJson(widget.room.botOptions?.toJson())
|
||||
: BotOptionsModel();
|
||||
|
||||
widget.room.botIsInRoom.then((bool isBotRoom) {
|
||||
setState(() => addBot = isBotRoom);
|
||||
});
|
||||
|
||||
discussionKeywordsController.text = botOptions.discussionKeywords ?? "";
|
||||
discussionTopicController.text = botOptions.discussionTopic ?? "";
|
||||
customSystemPromptController.text = botOptions.customSystemPrompt ?? "";
|
||||
}
|
||||
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
|
||||
void updateFromTextControllers() {
|
||||
botOptions.discussionTopic = discussionTopicController.text;
|
||||
botOptions.discussionKeywords = discussionKeywordsController.text;
|
||||
botOptions.customSystemPrompt = customSystemPromptController.text;
|
||||
}
|
||||
|
||||
void onUpdateChatMode(String? mode) {
|
||||
setState(() => botOptions.mode = mode ?? BotMode.discussion);
|
||||
}
|
||||
|
||||
void onUpdateBotLanguage(String? language) {
|
||||
setState(() => botOptions.targetLanguage = language);
|
||||
}
|
||||
|
||||
void onUpdateBotVoice(String? voice) {
|
||||
setState(() => botOptions.targetVoice = voice);
|
||||
}
|
||||
|
||||
void onUpdateBotLanguageLevel(int? level) {
|
||||
setState(() => botOptions.languageLevel = level);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dialogContent = Form(
|
||||
key: formKey,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
constraints: kIsWeb
|
||||
? const BoxConstraints(
|
||||
maxWidth: 450,
|
||||
maxHeight: 725,
|
||||
)
|
||||
: null,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
),
|
||||
child: Text(
|
||||
L10n.of(context)!.botConfig,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.of(context).pop(null),
|
||||
),
|
||||
],
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.conversationBotStatus,
|
||||
),
|
||||
value: addBot,
|
||||
onChanged: (bool value) {
|
||||
setState(() => addBot = value);
|
||||
},
|
||||
contentPadding: const EdgeInsets.all(4),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
AnimatedOpacity(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
opacity: addBot ? 1.0 : 0.5,
|
||||
child: ConversationBotSettingsForm(
|
||||
botOptions: botOptions,
|
||||
discussionKeywordsController:
|
||||
discussionKeywordsController,
|
||||
discussionTopicController: discussionTopicController,
|
||||
customSystemPromptController:
|
||||
customSystemPromptController,
|
||||
enabled: addBot,
|
||||
onUpdateBotMode: onUpdateChatMode,
|
||||
onUpdateBotLanguage: onUpdateBotLanguage,
|
||||
onUpdateBotVoice: onUpdateBotVoice,
|
||||
onUpdateBotLanguageLevel: onUpdateBotLanguageLevel,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(null);
|
||||
},
|
||||
child: Text(L10n.of(context)!.cancel),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final isValid = formKey.currentState!.validate();
|
||||
if (!isValid) return;
|
||||
|
||||
updateFromTextControllers();
|
||||
|
||||
final bool isBotRoomMember =
|
||||
await widget.room.botIsInRoom;
|
||||
if (addBot && !isBotRoomMember) {
|
||||
await widget.room.invite(BotName.byEnvironment);
|
||||
} else if (!addBot && isBotRoomMember) {
|
||||
await widget.room.kick(BotName.byEnvironment);
|
||||
}
|
||||
|
||||
Navigator.of(context).pop(botOptions);
|
||||
},
|
||||
child: Text(L10n.of(context)!.confirm),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return kIsWeb
|
||||
? Dialog(child: dialogContent)
|
||||
: Dialog.fullscreen(child: dialogContent);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,87 +1,110 @@
|
|||
import 'package:fluffychat/pangea/constants/bot_mode.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';
|
||||
import 'package:fluffychat/pangea/widgets/space/language_level_dropdown.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class ConversationBotSettingsForm extends StatefulWidget {
|
||||
class ConversationBotSettingsForm extends StatelessWidget {
|
||||
final BotOptionsModel botOptions;
|
||||
|
||||
final TextEditingController discussionTopicController;
|
||||
final TextEditingController discussionKeywordsController;
|
||||
final TextEditingController customSystemPromptController;
|
||||
|
||||
final bool enabled;
|
||||
final void Function(String?) onUpdateBotMode;
|
||||
final void Function(String?) onUpdateBotLanguage;
|
||||
final void Function(String?) onUpdateBotVoice;
|
||||
final void Function(int?) onUpdateBotLanguageLevel;
|
||||
|
||||
const ConversationBotSettingsForm({
|
||||
super.key,
|
||||
required this.botOptions,
|
||||
required this.discussionTopicController,
|
||||
required this.discussionKeywordsController,
|
||||
required this.customSystemPromptController,
|
||||
required this.onUpdateBotMode,
|
||||
required this.onUpdateBotLanguage,
|
||||
required this.onUpdateBotVoice,
|
||||
required this.onUpdateBotLanguageLevel,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
ConversationBotSettingsFormState createState() =>
|
||||
ConversationBotSettingsFormState();
|
||||
}
|
||||
|
||||
class ConversationBotSettingsFormState
|
||||
extends State<ConversationBotSettingsForm> {
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
late BotOptionsModel botOptions;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
botOptions = widget.botOptions;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
L10n.of(context)!.conversationLanguageLevel,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
DropdownButtonFormField(
|
||||
// Initial Value
|
||||
hint: Text(
|
||||
L10n.of(context)!.selectBotLanguage,
|
||||
overflow: TextOverflow.clip,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
value: botOptions.targetLanguage,
|
||||
isExpanded: true,
|
||||
icon: const Icon(Icons.keyboard_arrow_down),
|
||||
items: MatrixState.pangeaController.pLanguageStore.targetOptions
|
||||
.map((language) {
|
||||
return DropdownMenuItem(
|
||||
value: language.langCode,
|
||||
child: Text(
|
||||
language.getDisplayName(context) ?? language.langCode,
|
||||
overflow: TextOverflow.clip,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: enabled ? onUpdateBotLanguage : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<String>(
|
||||
// Initial Value
|
||||
hint: Text(
|
||||
L10n.of(context)!.chooseVoice,
|
||||
overflow: TextOverflow.clip,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
value: botOptions.targetVoice,
|
||||
isExpanded: true,
|
||||
icon: const Icon(Icons.keyboard_arrow_down),
|
||||
items: const [],
|
||||
onChanged: enabled ? onUpdateBotVoice : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
LanguageLevelDropdown(
|
||||
initialLevel: botOptions.languageLevel,
|
||||
onChanged: (int? newValue) => {
|
||||
setState(() {
|
||||
botOptions.languageLevel = newValue!;
|
||||
}),
|
||||
},
|
||||
onChanged: onUpdateBotLanguageLevel,
|
||||
validator: (value) => enabled && value == null
|
||||
? L10n.of(context)!.enterLanguageLevel
|
||||
: null,
|
||||
enabled: enabled,
|
||||
),
|
||||
Text(
|
||||
L10n.of(context)!.conversationBotModeSelectDescription,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
const SizedBox(height: 12),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Text(
|
||||
L10n.of(context)!.conversationBotModeSelectDescription,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
ConversationBotModeSelect(
|
||||
initialMode: botOptions.mode,
|
||||
onChanged: (String? mode) => {
|
||||
setState(() {
|
||||
botOptions.mode = mode ?? BotMode.discussion;
|
||||
}),
|
||||
},
|
||||
onChanged: onUpdateBotMode,
|
||||
enabled: enabled,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: ConversationBotModeDynamicZone(
|
||||
initialBotOptions: botOptions,
|
||||
onChanged: (BotOptionsModel? newOptions) {
|
||||
if (newOptions != null) {
|
||||
setState(() {
|
||||
botOptions = newOptions;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ConversationBotModeDynamicZone(
|
||||
botOptions: botOptions,
|
||||
discussionTopicController: discussionTopicController,
|
||||
discussionKeywordsController: discussionKeywordsController,
|
||||
customSystemPromptController: customSystemPromptController,
|
||||
enabled: enabled,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import 'package:fluffychat/pangea/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_label.dart';
|
||||
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_title.dart';
|
||||
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_text_adventure_game_master_instruction_input.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
// TODO check how this looks
|
||||
class ConversationBotTextAdventureZone extends StatelessWidget {
|
||||
final BotOptionsModel initialBotOptions;
|
||||
// call this to update propagate changes to parents
|
||||
|
|
@ -20,13 +18,6 @@ class ConversationBotTextAdventureZone extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
ConversationBotDynamicZoneTitle(
|
||||
title: L10n.of(context)!.conversationBotTextAdventureZone_title,
|
||||
),
|
||||
ConversationBotDynamicZoneLabel(
|
||||
label: L10n.of(context)!
|
||||
.conversationBotTextAdventureZone_instructionLabel,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: ConversationBotGameMasterInstructionsInput(
|
||||
|
|
|
|||
|
|
@ -244,6 +244,7 @@ class WordMatchContent extends StatelessWidget {
|
|||
controller: scrollController,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// const SizedBox(height: 10.0),
|
||||
// if (matchCopy.description != null)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ import 'dart:developer';
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart';
|
||||
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/practice_activity/practice_activity_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/word_audio_button.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -13,7 +15,7 @@ import 'package:flutter/material.dart';
|
|||
/// The multiple choice activity view
|
||||
class MultipleChoiceActivity extends StatefulWidget {
|
||||
final MessagePracticeActivityCardState practiceCardController;
|
||||
final PracticeActivityModel? currentActivity;
|
||||
final PracticeActivityModel currentActivity;
|
||||
|
||||
const MultipleChoiceActivity({
|
||||
super.key,
|
||||
|
|
@ -52,7 +54,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
}
|
||||
|
||||
final bool isCorrect =
|
||||
widget.currentActivity!.multipleChoice!.isCorrect(value, index);
|
||||
widget.currentActivity.content.isCorrect(value, index);
|
||||
|
||||
currentRecordModel?.addResponse(
|
||||
text: value,
|
||||
|
|
@ -79,7 +81,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
);
|
||||
|
||||
// If the selected choice is correct, send the record and get the next activity
|
||||
if (widget.currentActivity!.multipleChoice!.isCorrect(value, index)) {
|
||||
if (widget.currentActivity.content.isCorrect(value, index)) {
|
||||
widget.practiceCardController.onActivityFinish();
|
||||
}
|
||||
|
||||
|
|
@ -90,39 +92,37 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final PracticeActivityModel? practiceActivity = widget.currentActivity;
|
||||
|
||||
if (practiceActivity == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
final PracticeActivityModel practiceActivity = widget.currentActivity;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
practiceActivity.multipleChoice!.question,
|
||||
practiceActivity.content.question,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (practiceActivity.activityType ==
|
||||
ActivityTypeEnum.wordFocusListening)
|
||||
WordAudioButton(text: practiceActivity.content.answer),
|
||||
ChoicesArray(
|
||||
isLoading: false,
|
||||
uniqueKeyForLayerLink: (index) => "multiple_choice_$index",
|
||||
originalSpan: "placeholder",
|
||||
onPressed: updateChoice,
|
||||
selectedChoiceIndex: selectedChoiceIndex,
|
||||
choices: practiceActivity.multipleChoice!.choices
|
||||
choices: practiceActivity.content.choices
|
||||
.mapIndexed(
|
||||
(index, value) => Choice(
|
||||
text: value,
|
||||
color: currentRecordModel?.hasTextResponse(value) ?? false
|
||||
? practiceActivity.multipleChoice!.choiceColor(index)
|
||||
? practiceActivity.content.choiceColor(index)
|
||||
: null,
|
||||
isGold: practiceActivity.multipleChoice!
|
||||
.isCorrect(value, index),
|
||||
isGold: practiceActivity.content.isCorrect(value, index),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
|
|
|
|||
|
|
@ -231,10 +231,6 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
return;
|
||||
}
|
||||
|
||||
// clear the current activity and record
|
||||
currentActivity = null;
|
||||
currentCompletionRecord = null;
|
||||
|
||||
_fetchNewActivity(
|
||||
ActivityQualityFeedback(
|
||||
feedbackText: feedback,
|
||||
|
|
@ -281,7 +277,14 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
case ActivityTypeEnum.multipleChoice:
|
||||
return MultipleChoiceActivity(
|
||||
practiceCardController: this,
|
||||
currentActivity: currentActivity,
|
||||
currentActivity: currentActivity!,
|
||||
);
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
// return WordFocusListeningActivity(
|
||||
// activity: currentActivity!, practiceCardController: this);
|
||||
return MultipleChoiceActivity(
|
||||
practiceCardController: this,
|
||||
currentActivity: currentActivity!,
|
||||
);
|
||||
default:
|
||||
ErrorHandler.logError(
|
||||
|
|
|
|||
|
|
@ -58,17 +58,9 @@ class TargetTokensController {
|
|||
return _targetTokens = [];
|
||||
}
|
||||
|
||||
_targetTokens = [];
|
||||
for (int i = 0; i < tokens.length; i++) {
|
||||
//don't bother with tokens that we don't save to vocab
|
||||
if (!tokens[i].lemma.saveVocab) {
|
||||
continue;
|
||||
}
|
||||
|
||||
_targetTokens!.add(tokens[i].emptyTokenWithXP);
|
||||
}
|
||||
|
||||
return _targetTokens!;
|
||||
return _targetTokens = tokens
|
||||
.map((token) => token.emptyTokenWithXP)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<void> updateTokensWithConstructs(
|
||||
|
|
@ -84,6 +76,12 @@ class TargetTokensController {
|
|||
_targetTokens ??= await _initialize(context, pangeaMessageEvent);
|
||||
|
||||
for (final token in _targetTokens!) {
|
||||
|
||||
// we don't need to do this for tokens that don't have saveVocab set to true
|
||||
if (!token.token.lemma.saveVocab){
|
||||
continue;
|
||||
}
|
||||
|
||||
for (final construct in token.constructs) {
|
||||
final constructUseModel = constructList.getConstructUses(
|
||||
construct.id.lemma,
|
||||
|
|
|
|||
74
lib/pangea/widgets/practice_activity/word_audio_button.dart
Normal file
74
lib/pangea/widgets/practice_activity/word_audio_button.dart
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class WordAudioButton extends StatefulWidget {
|
||||
final String text;
|
||||
|
||||
const WordAudioButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
@override
|
||||
WordAudioButtonState createState() => WordAudioButtonState();
|
||||
}
|
||||
|
||||
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) {
|
||||
return Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.play_arrow_outlined),
|
||||
isSelected: _isPlaying,
|
||||
selectedIcon: const Icon(Icons.pause_outlined),
|
||||
color: _isPlaying ? Colors.white : null,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
_isPlaying
|
||||
? Theme.of(context).colorScheme.secondary
|
||||
: Theme.of(context).colorScheme.primaryContainer,
|
||||
),
|
||||
),
|
||||
tooltip:
|
||||
_isPlaying ? L10n.of(context)!.stop : L10n.of(context)!.playAudio,
|
||||
onPressed: () async {
|
||||
if (_isPlaying) {
|
||||
await ttsController.tts.stop();
|
||||
if (mounted) {
|
||||
setState(() => _isPlaying = false);
|
||||
}
|
||||
} else {
|
||||
if (mounted) {
|
||||
setState(() => _isPlaying = true);
|
||||
}
|
||||
await ttsController.speak(widget.text);
|
||||
if (mounted) {
|
||||
setState(() => _isPlaying = false);
|
||||
}
|
||||
}
|
||||
}, // Disable button if language isn't supported
|
||||
),
|
||||
ttsController.missingVoiceButton,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.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/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WordFocusListeningActivity extends StatefulWidget {
|
||||
final PracticeActivityModel activity;
|
||||
final MessagePracticeActivityCardState practiceCardController;
|
||||
|
||||
const WordFocusListeningActivity({
|
||||
super.key,
|
||||
required this.activity,
|
||||
required this.practiceCardController,
|
||||
});
|
||||
|
||||
@override
|
||||
WordFocusListeningActivityState createState() =>
|
||||
WordFocusListeningActivityState();
|
||||
|
||||
ActivityContent get activityContent => activity.content;
|
||||
}
|
||||
|
||||
class WordFocusListeningActivityState
|
||||
extends State<WordFocusListeningActivity> {
|
||||
int? selectedChoiceIndex;
|
||||
|
||||
TtsController tts = TtsController();
|
||||
|
||||
final double buttonSize = 40;
|
||||
|
||||
PracticeActivityRecordModel? get currentRecordModel =>
|
||||
widget.practiceCardController.currentCompletionRecord;
|
||||
|
||||
initializeTTS() async {
|
||||
tts.setupTTS().then((value) => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initializeTTS();
|
||||
}
|
||||
|
||||
void checkAnswer(int index) {
|
||||
final String value = widget.activityContent.choices[index];
|
||||
|
||||
if (currentRecordModel?.hasTextResponse(value) ?? false) {
|
||||
return;
|
||||
}
|
||||
|
||||
final bool isCorrect = widget.activity.content.isCorrect(value, index);
|
||||
|
||||
currentRecordModel?.addResponse(
|
||||
text: value,
|
||||
score: isCorrect ? 1 : 0,
|
||||
);
|
||||
|
||||
if (currentRecordModel == null ||
|
||||
currentRecordModel!.latestResponse == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
MatrixState.pangeaController.myAnalytics.setState(
|
||||
AnalyticsStream(
|
||||
// note - this maybe should be the activity event id
|
||||
eventId:
|
||||
widget.practiceCardController.widget.pangeaMessageEvent.eventId,
|
||||
roomId: widget.practiceCardController.widget.pangeaMessageEvent.room.id,
|
||||
constructs: currentRecordModel!.latestResponse!.toUses(
|
||||
widget.practiceCardController.currentActivity!,
|
||||
widget.practiceCardController.metadata,
|
||||
),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
selectedChoiceIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Column(
|
||||
children: [
|
||||
// Text question at the top
|
||||
Text(
|
||||
widget.activityContent.question,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Blank slot for the answer
|
||||
DragTarget<int>(
|
||||
builder: (context, candidateData, rejectedData) {
|
||||
return CircleAvatar(
|
||||
radius: buttonSize,
|
||||
backgroundColor: Colors.transparent,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AppConfig.primaryColor.withOpacity(0.4),
|
||||
width: 2,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onAcceptWithDetails: (details) => checkAnswer(details.data),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Audio options as draggable buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: List.generate(
|
||||
widget.activityContent.choices.length,
|
||||
(index) => Draggable<int>(
|
||||
data: index,
|
||||
feedback: _buildAudioButton(context, theme, index),
|
||||
childWhenDragging: _buildAudioButton(context, theme, index, true),
|
||||
child: _buildAudioButton(context, theme, index),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Helper method to build the audio buttons
|
||||
Widget _buildAudioButton(
|
||||
BuildContext context,
|
||||
ThemeData theme,
|
||||
int index, [
|
||||
bool dragging = false,
|
||||
]) {
|
||||
final isAnswerCorrect = widget.activityContent.isCorrect(
|
||||
widget.activityContent.choices[index],
|
||||
index,
|
||||
);
|
||||
Color buttonColor;
|
||||
if (selectedChoiceIndex == index) {
|
||||
buttonColor = isAnswerCorrect
|
||||
? theme.colorScheme.secondary.withOpacity(0.7) // Correct: Green
|
||||
: theme.colorScheme.error.withOpacity(0.7); // Incorrect: Red
|
||||
} else {
|
||||
buttonColor =
|
||||
AppConfig.primaryColor.withOpacity(0.4); // Default: Primary color
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => tts.speak(widget.activityContent.choices[index]),
|
||||
child: CircleAvatar(
|
||||
radius: buttonSize,
|
||||
backgroundColor: dragging ? Colors.grey.withOpacity(0.5) : buttonColor,
|
||||
child: const Icon(Icons.play_arrow),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,71 +6,46 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|||
class LanguageLevelDropdown extends StatelessWidget {
|
||||
final int? initialLevel;
|
||||
final void Function(int?)? onChanged;
|
||||
final String? Function(int?)? validator;
|
||||
final bool enabled;
|
||||
|
||||
const LanguageLevelDropdown({
|
||||
super.key,
|
||||
this.initialLevel,
|
||||
this.onChanged,
|
||||
this.validator,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
width: 0.5,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
child: DropdownButton(
|
||||
// Initial Value
|
||||
hint: Padding(
|
||||
padding: const EdgeInsets.only(left: 15),
|
||||
child: Text(
|
||||
initialLevel == null
|
||||
? L10n.of(context)!.selectLanguageLevel
|
||||
: LanguageLevelTextPicker.languageLevelText(
|
||||
context,
|
||||
initialLevel!,
|
||||
),
|
||||
style: const TextStyle().copyWith(
|
||||
color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
fontSize: 14,
|
||||
),
|
||||
overflow: TextOverflow.clip,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
isExpanded: true,
|
||||
underline: Container(),
|
||||
// Down Arrow Icon
|
||||
icon: const Icon(Icons.keyboard_arrow_down),
|
||||
// Array list of items
|
||||
items: LanguageLevelType.allInts.map((int levelOption) {
|
||||
return DropdownMenuItem(
|
||||
value: levelOption,
|
||||
child: Text(
|
||||
LanguageLevelTextPicker.languageLevelText(
|
||||
context,
|
||||
levelOption,
|
||||
),
|
||||
style: const TextStyle().copyWith(
|
||||
color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
fontSize: 14,
|
||||
),
|
||||
overflow: TextOverflow.clip,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
// After selecting the desired option,it will
|
||||
// change button value to selected value
|
||||
onChanged: onChanged,
|
||||
),
|
||||
return DropdownButtonFormField(
|
||||
// Initial Value
|
||||
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,
|
||||
child: Text(
|
||||
LanguageLevelTextPicker.languageLevelText(
|
||||
context,
|
||||
levelOption,
|
||||
),
|
||||
overflow: TextOverflow.clip,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: enabled ? onChanged : null,
|
||||
validator: validator,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
78
lib/utils/file_selector.dart
Normal file
78
lib/utils/file_selector.dart
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/app_lock.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
Future<List<XFile>> selectFiles(
|
||||
BuildContext context, {
|
||||
String? title,
|
||||
FileSelectorType type = FileSelectorType.any,
|
||||
bool allowMultiple = false,
|
||||
}) async {
|
||||
if (!PlatformInfos.isLinux) {
|
||||
final result = await AppLock.of(context).pauseWhile(
|
||||
FilePicker.platform.pickFiles(
|
||||
compressionQuality: 0,
|
||||
allowMultiple: allowMultiple,
|
||||
type: type.filePickerType,
|
||||
allowedExtensions: type.extensions,
|
||||
),
|
||||
);
|
||||
return result?.xFiles ?? [];
|
||||
}
|
||||
|
||||
if (allowMultiple) {
|
||||
return await AppLock.of(context).pauseWhile(
|
||||
openFiles(
|
||||
confirmButtonText: title,
|
||||
acceptedTypeGroups: type.groups,
|
||||
),
|
||||
);
|
||||
}
|
||||
final file = await AppLock.of(context).pauseWhile(
|
||||
openFile(
|
||||
confirmButtonText: title,
|
||||
acceptedTypeGroups: type.groups,
|
||||
),
|
||||
);
|
||||
if (file == null) return [];
|
||||
return [file];
|
||||
}
|
||||
|
||||
enum FileSelectorType {
|
||||
any([], FileType.any, null),
|
||||
images(
|
||||
[
|
||||
XTypeGroup(
|
||||
label: 'JPG',
|
||||
extensions: <String>['jpg', 'JPG', 'jpeg', 'JPEG'],
|
||||
),
|
||||
XTypeGroup(
|
||||
label: 'PNGs',
|
||||
extensions: <String>['png', 'PNG'],
|
||||
),
|
||||
XTypeGroup(
|
||||
label: 'WEBP',
|
||||
extensions: <String>['WebP', 'WEBP'],
|
||||
),
|
||||
],
|
||||
FileType.image,
|
||||
null,
|
||||
),
|
||||
zip(
|
||||
[
|
||||
XTypeGroup(
|
||||
label: 'ZIP',
|
||||
extensions: <String>['zip', 'ZIP'],
|
||||
),
|
||||
],
|
||||
FileType.custom,
|
||||
['zip', 'ZIP'],
|
||||
);
|
||||
|
||||
const FileSelectorType(this.groups, this.filePickerType, this.extensions);
|
||||
final List<XTypeGroup> groups;
|
||||
final FileType filePickerType;
|
||||
final List<String>? extensions;
|
||||
}
|
||||
|
|
@ -18,7 +18,12 @@ abstract class UpdateNotifier {
|
|||
ScaffoldFeatureController? controller;
|
||||
controller = scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
duration: const Duration(seconds: 30),
|
||||
duration: const Duration(
|
||||
// #Pangea
|
||||
// seconds: 30,
|
||||
seconds: 5,
|
||||
// Pangea#
|
||||
),
|
||||
content: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.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';
|
||||
|
|
@ -18,7 +19,6 @@ enum ChatPopupMenuActions {
|
|||
leave,
|
||||
search,
|
||||
// #Pangea
|
||||
archive,
|
||||
downloadTxt,
|
||||
downloadCsv,
|
||||
downloadXlsx,
|
||||
|
|
@ -118,25 +118,6 @@ class ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
|
|||
context.go('/rooms/${widget.room.id}/search');
|
||||
break;
|
||||
// #Pangea
|
||||
case ChatPopupMenuActions.archive:
|
||||
final confirmed = await showOkCancelAlertDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context)!.areYouSure,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
message: L10n.of(context)!.archiveRoomDescription,
|
||||
);
|
||||
if (confirmed == OkCancelResult.ok) {
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => widget.room.archive(),
|
||||
);
|
||||
if (success.error == null) {
|
||||
context.go('/rooms');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ChatPopupMenuActions.downloadTxt:
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
|
|
@ -171,7 +152,30 @@ class ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
|
|||
);
|
||||
break;
|
||||
case ChatPopupMenuActions.learningSettings:
|
||||
context.go('/rooms/settings/learning');
|
||||
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),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
break;
|
||||
// Pangea#
|
||||
}
|
||||
|
|
@ -246,18 +250,6 @@ class ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
|
|||
),
|
||||
),
|
||||
// #Pangea
|
||||
if (!widget.room.isArchived)
|
||||
if (widget.room.isRoomAdmin)
|
||||
PopupMenuItem<ChatPopupMenuActions>(
|
||||
value: ChatPopupMenuActions.archive,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.archive_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.archive),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem<ChatPopupMenuActions>(
|
||||
value: ChatPopupMenuActions.downloadTxt,
|
||||
child: Row(
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import firebase_messaging
|
|||
import flutter_app_badger
|
||||
import flutter_local_notifications
|
||||
import flutter_secure_storage_macos
|
||||
import flutter_tts
|
||||
import flutter_web_auth_2
|
||||
import flutter_webrtc
|
||||
import geolocator_apple
|
||||
|
|
@ -54,6 +55,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||
FlutterAppBadgerPlugin.register(with: registry.registrar(forPlugin: "FlutterAppBadgerPlugin"))
|
||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin"))
|
||||
FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin"))
|
||||
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
|
||||
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
||||
|
|
|
|||
61
pubspec.lock
61
pubspec.lock
|
|
@ -33,6 +33,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.1"
|
||||
android_intent_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: android_intent_plus
|
||||
sha256: "38921ec22ebb3b9a7eb678792cf6fab0b6f458b61b9d327688573449c9b47db3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.2.0"
|
||||
animations:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -416,6 +424,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.0.6"
|
||||
file_selector:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_selector
|
||||
sha256: "5019692b593455127794d5718304ff1ae15447dea286cdda9f0db2a796a1b828"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
file_selector_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_android
|
||||
sha256: "00aafa9ae05a8663d0b4f17abd2a02316911ca0f46f9b9dacb9578b324d99590"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.1+9"
|
||||
file_selector_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_ios
|
||||
sha256: "94b98ad950b8d40d96fee8fa88640c2e4bd8afcdd4817993bd04e20310f45420"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.3+1"
|
||||
file_selector_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -440,6 +472,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.2"
|
||||
file_selector_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_web
|
||||
sha256: c4c0ea4224d97a60a7067eca0c8fd419e708ff830e0c83b11a48faf566cec3e7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4+2"
|
||||
file_selector_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -829,6 +869,14 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_tts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_tts
|
||||
sha256: aed2a00c48c43af043ed81145fd8503ddd793dafa7088ab137dbef81a703e53d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
flutter_typeahead:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -1276,15 +1324,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
keyboard_shortcuts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: null-safety
|
||||
resolved-ref: a3d4020911860ff091d90638ab708604b71d2c5a
|
||||
url: "https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git"
|
||||
source: git
|
||||
version: "0.1.4"
|
||||
language_tool:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -2740,5 +2779,5 @@ packages:
|
|||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.4.0 <4.0.0"
|
||||
flutter: ">=3.22.0"
|
||||
dart: ">=3.5.0 <4.0.0"
|
||||
flutter: ">=3.24.0"
|
||||
|
|
|
|||
23
pubspec.yaml
23
pubspec.yaml
|
|
@ -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.4+3540
|
||||
version: 1.21.5+3541
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
|
@ -32,6 +32,7 @@ dependencies:
|
|||
emojis: ^0.9.9
|
||||
#fcm_shared_isolate: ^0.1.0
|
||||
file_picker: ^8.0.6
|
||||
file_selector: ^1.0.3
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_app_badger: ^1.5.0
|
||||
|
|
@ -49,7 +50,7 @@ dependencies:
|
|||
flutter_olm: 1.3.2 # Keep in sync with scripts/prepare-web.sh ! 1.4.0 does currently not build on Android
|
||||
flutter_openssl_crypto: ^0.3.0
|
||||
flutter_ringtone_player: ^4.0.0+2
|
||||
flutter_secure_storage: ^9.0.0
|
||||
flutter_secure_storage: ^9.2.2
|
||||
flutter_shortcuts:
|
||||
git: https://github.com/krille-chan/flutter_shortcuts.git
|
||||
flutter_typeahead: ^5.2.0
|
||||
|
|
@ -67,7 +68,9 @@ dependencies:
|
|||
image_picker: ^1.1.0
|
||||
intl: any
|
||||
just_audio: ^0.9.39
|
||||
keyboard_shortcuts: ^0.1.4
|
||||
# #Pangea
|
||||
# keyboard_shortcuts: ^0.1.4
|
||||
# Pangea#
|
||||
latlong2: ^0.9.1
|
||||
linkify: ^5.0.0
|
||||
# #Pangea
|
||||
|
|
@ -107,6 +110,7 @@ dependencies:
|
|||
wakelock_plus: ^1.2.2
|
||||
webrtc_interface: ^1.0.13
|
||||
# #Pangea
|
||||
android_intent_plus: ^5.2.0
|
||||
country_picker: ^2.0.25
|
||||
csv: ^6.0.0
|
||||
fl_chart: ^0.67.0
|
||||
|
|
@ -128,6 +132,7 @@ dependencies:
|
|||
shimmer: ^3.0.0
|
||||
syncfusion_flutter_xlsio: ^25.1.40
|
||||
rive: 0.11.11
|
||||
flutter_tts: ^4.0.2
|
||||
# Pangea#
|
||||
|
||||
dev_dependencies:
|
||||
|
|
@ -213,8 +218,10 @@ dependency_overrides:
|
|||
version: ^1.0.1
|
||||
# waiting for null safety
|
||||
# Upstream pull request: https://github.com/AntoineMarcel/keyboard_shortcuts/pull/13
|
||||
keyboard_shortcuts:
|
||||
git:
|
||||
url: https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git
|
||||
ref: null-safety
|
||||
win32: 5.5.3
|
||||
# #Pangea
|
||||
# keyboard_shortcuts:
|
||||
# git:
|
||||
# url: https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git
|
||||
# ref: null-safety
|
||||
# win32: 5.5.3
|
||||
# Pangea#
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <firebase_core/firebase_core_plugin_c_api.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <flutter_tts/flutter_tts_plugin.h>
|
||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||
#include <pasteboard/pasteboard_plugin.h>
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
|
|
@ -33,6 +34,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
FlutterTtsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterTtsPlugin"));
|
||||
FlutterWebRTCPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
|
||||
PasteboardPluginRegisterWithRegistrar(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||
file_selector_windows
|
||||
firebase_core
|
||||
flutter_secure_storage_windows
|
||||
flutter_tts
|
||||
flutter_webrtc
|
||||
pasteboard
|
||||
permission_handler_windows
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue