Merge branch 'main' into 5421-grammar-practice-todos
This commit is contained in:
commit
aa597b8698
98 changed files with 2811 additions and 800 deletions
|
|
@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix_api_lite/generated/model.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
|
|
@ -48,6 +47,7 @@ import 'package:fluffychat/pangea/chat_settings/pages/pangea_invitation_selectio
|
|||
import 'package:fluffychat/pangea/common/utils/p_vguard.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/course_creation/course_invite_page.dart';
|
||||
import 'package:fluffychat/pangea/course_creation/public_course_preview.dart';
|
||||
import 'package:fluffychat/pangea/course_creation/selected_course_page.dart';
|
||||
import 'package:fluffychat/pangea/join_codes/join_with_link_page.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/settings_learning.dart';
|
||||
|
|
@ -407,15 +407,13 @@ abstract class AppRoutes {
|
|||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: ':courseid',
|
||||
path: ':courseroomid',
|
||||
pageBuilder: (context, state) {
|
||||
return defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
SelectedCourse(
|
||||
state.pathParameters['courseid']!,
|
||||
SelectedCourseMode.join,
|
||||
roomChunk: state.extra as PublicRoomsChunk?,
|
||||
PublicCoursePreview(
|
||||
roomID: state.pathParameters['courseroomid']!,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"@@locale": "ar",
|
||||
"@@last_modified": "2026-01-27 14:02:59.727958",
|
||||
"@@last_modified": "2026-01-28 13:26:35.542116",
|
||||
"about": "حول",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -11151,5 +11151,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "ما اللغة التي تتعلمها؟",
|
||||
"searchLanguagesHint": "ابحث عن اللغات المستهدفة",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "أسئلة؟ نحن هنا للمساعدة!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "حدث خطأ ما، ونحن نعمل بجد على إصلاحه. تحقق مرة أخرى لاحقًا.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1910,7 +1910,7 @@
|
|||
"playWithAI": "Пакуль гуляйце з ШІ",
|
||||
"courseStartDesc": "Pangea Bot гатовы да працы ў любы час!\n\n...але навучанне лепш з сябрамі!",
|
||||
"@@locale": "be",
|
||||
"@@last_modified": "2026-01-27 14:02:50.270963",
|
||||
"@@last_modified": "2026-01-28 13:26:22.828870",
|
||||
"@alwaysUse24HourFormat": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
|
|
@ -12033,5 +12033,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Якую мову вы вывучаеце?",
|
||||
"searchLanguagesHint": "Пошук мэтавых моў",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Пытанні? Мы тут, каб дапамагчы!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Што-то пайшло не так, і мы актыўна працуем над выпраўленнем. Праверце пазней.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"@@last_modified": "2026-01-27 14:03:11.965364",
|
||||
"@@last_modified": "2026-01-28 13:26:47.712647",
|
||||
"about": "সম্পর্কে",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -12038,5 +12038,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "আপনি কোন ভাষা শিখছেন?",
|
||||
"searchLanguagesHint": "লক্ষ্য ভাষা অনুসন্ধান করুন",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "প্রশ্ন আছে? আমরা সাহায্য করতে এখানে আছি!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "কিছু ভুল হয়েছে, এবং আমরা এটি ঠিক করতে কঠোর পরিশ্রম করছি। পরে আবার চেক করুন।",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -4278,7 +4278,7 @@
|
|||
"joinPublicTrip": "མི་ཚེས་ལ་ལོག་འབད།",
|
||||
"startOwnTrip": "ངེད་རང་གི་ལོག་ལ་སྦྱོར་བཅོས།",
|
||||
"@@locale": "bo",
|
||||
"@@last_modified": "2026-01-27 14:03:09.952724",
|
||||
"@@last_modified": "2026-01-28 13:26:44.570789",
|
||||
"@alwaysUse24HourFormat": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
|
|
@ -10688,5 +10688,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Kedua bahasa apa yang Anda pelajari?",
|
||||
"searchLanguagesHint": "Cari bahasa target",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Pytania? Jesteśmy tutaj, aby pomóc!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Nǐng bǐng wǒng, yǐng wǒng bǐng wǒng. Cǐng bǐng yǐng bǐng.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"@@last_modified": "2026-01-27 14:02:51.719519",
|
||||
"@@last_modified": "2026-01-28 13:26:23.872618",
|
||||
"about": "Quant a",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -10958,5 +10958,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Quina llengua estàs aprenent?",
|
||||
"searchLanguagesHint": "Cerca llengües objectiu",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Preguntes? Som aquí per ajudar!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Alguna cosa ha anat malament, i estem treballant dur per solucionar-ho. Comprova-ho més tard.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"@@locale": "cs",
|
||||
"@@last_modified": "2026-01-27 14:02:46.567796",
|
||||
"@@last_modified": "2026-01-28 13:26:20.564591",
|
||||
"about": "O aplikaci",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -11541,5 +11541,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Jaký jazyk se učíte?",
|
||||
"searchLanguagesHint": "Hledejte cílové jazyky",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Otázky? Jsme tu, abychom pomohli!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Něco se pokazilo a my na tom tvrdě pracujeme. Zkontrolujte to prosím později.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1929,7 +1929,7 @@
|
|||
"playWithAI": "Leg med AI for nu",
|
||||
"courseStartDesc": "Pangea Bot er klar til at starte når som helst!\n\n...men læring er bedre med venner!",
|
||||
"@@locale": "da",
|
||||
"@@last_modified": "2026-01-27 14:02:21.118853",
|
||||
"@@last_modified": "2026-01-28 13:25:58.575899",
|
||||
"@aboutHomeserver": {
|
||||
"type": "String",
|
||||
"placeholders": {
|
||||
|
|
@ -11995,5 +11995,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Hvilket sprog lærer du?",
|
||||
"searchLanguagesHint": "Søg efter målsprog",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Spørgsmål? Vi er her for at hjælpe!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Noget gik galt, og vi arbejder hårdt på at løse det. Tjek igen senere.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"@@locale": "de",
|
||||
"@@last_modified": "2026-01-27 14:02:39.850908",
|
||||
"@@last_modified": "2026-01-28 13:26:14.577124",
|
||||
"alwaysUse24HourFormat": "true",
|
||||
"@alwaysUse24HourFormat": {
|
||||
"description": "Set to true to always display time of day in 24 hour format."
|
||||
|
|
@ -10941,5 +10941,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Welche Sprache lernst du?",
|
||||
"searchLanguagesHint": "Zielsprachen suchen",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Fragen? Wir sind hier, um zu helfen!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Etwas ist schiefgelaufen, und wir arbeiten hart daran, es zu beheben. Überprüfen Sie es später erneut.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -4455,7 +4455,7 @@
|
|||
"playWithAI": "Παίξτε με την Τεχνητή Νοημοσύνη προς το παρόν",
|
||||
"courseStartDesc": "Ο Pangea Bot είναι έτοιμος να ξεκινήσει οποιαδήποτε στιγμή!\n\n...αλλά η μάθηση είναι καλύτερη με φίλους!",
|
||||
"@@locale": "el",
|
||||
"@@last_modified": "2026-01-27 14:03:17.212773",
|
||||
"@@last_modified": "2026-01-28 13:26:53.057151",
|
||||
"@alwaysUse24HourFormat": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
|
|
@ -11992,5 +11992,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Ποια γλώσσα μαθαίνετε;",
|
||||
"searchLanguagesHint": "Αναζητήστε γλώσσες στόχου",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Ερωτήσεις; Είμαστε εδώ για να βοηθήσουμε!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Κάτι πήγε στραβά και εργαζόμαστε σκληρά για να το διορθώσουμε. Έλεγξε ξανά αργότερα.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -5063,5 +5063,9 @@
|
|||
"publicInviteDescSpace": "Search for users to invite them to this space.",
|
||||
"useActivityImageAsChatBackground": "Use activity image as chat background",
|
||||
"chatWithSupport": "Chat with Support",
|
||||
"newCourseAccess": "By default, courses are publicly searchable and require admin approval to join. You can edit these settings at any time."
|
||||
"newCourseAccess": "By default, courses are publicly searchable and require admin approval to join. You can edit these settings at any time.",
|
||||
"courseLoadingError": "Something went wrong, and we're hard at work fixing it. Check again later.",
|
||||
"onboardingLanguagesTitle": "What language are you learning?",
|
||||
"searchLanguagesHint": "Search target languages",
|
||||
"supportSubtitle": "Questions? We're here to help!"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"@@last_modified": "2026-01-27 14:03:21.507072",
|
||||
"@@last_modified": "2026-01-28 13:26:57.219657",
|
||||
"about": "Prio",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -12023,5 +12023,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Kian lingvon vi lernas?",
|
||||
"searchLanguagesHint": "Serĉu celajn lingvojn",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Demandoj? Ni ĉi tie por helpi!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Io malĝuste okazis, kaj ni diligente laboras por ripari ĝin. Kontrolu denove poste.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"@@locale": "es",
|
||||
"@@last_modified": "2026-01-27 14:02:15.353643",
|
||||
"@@last_modified": "2026-01-28 13:25:54.826808",
|
||||
"about": "Acerca de",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -8168,5 +8168,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "¿Qué idioma estás aprendiendo?",
|
||||
"searchLanguagesHint": "Buscar idiomas objetivo",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "¿Preguntas? ¡Estamos aquí para ayudar!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Algo salió mal y estamos trabajando arduamente para solucionarlo. Revisa de nuevo más tarde.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"@@locale": "et",
|
||||
"@@last_modified": "2026-01-27 14:02:38.644251",
|
||||
"@@last_modified": "2026-01-28 13:26:13.431455",
|
||||
"about": "Rakenduse teave",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -11205,5 +11205,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Millist keelt sa õpid?",
|
||||
"searchLanguagesHint": "Otsi sihtkeeli",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Küsimused? Me oleme siin, et aidata!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Midagi läks valesti ja me teeme kõvasti tööd, et see parandada. Kontrolli hiljem uuesti.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"@@locale": "eu",
|
||||
"@@last_modified": "2026-01-27 14:02:35.628448",
|
||||
"@@last_modified": "2026-01-28 13:26:10.864458",
|
||||
"about": "Honi buruz",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -10934,5 +10934,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Zer hizkuntza ikasten ari zara?",
|
||||
"searchLanguagesHint": "Bilatu helburu hizkuntzak",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Galderak? Hemen gaude laguntzeko!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Zerbait oker joan da, eta horren konponketan lan gogorra egiten ari gara. Begiratu berriro geroago.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"@@last_modified": "2026-01-27 14:03:13.031797",
|
||||
"@@last_modified": "2026-01-28 13:26:49.168597",
|
||||
"repeatPassword": "تکرار رمزعبور",
|
||||
"@repeatPassword": {},
|
||||
"about": "درباره",
|
||||
|
|
@ -11666,5 +11666,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "شما در حال یادگیری چه زبانی هستید؟",
|
||||
"searchLanguagesHint": "زبانهای هدف را جستجو کنید",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "سوالات؟ ما اینجا هستیم تا کمک کنیم!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "مشکلی پیش آمده و ما در حال تلاش برای رفع آن هستیم. بعداً دوباره بررسی کنید.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -4008,7 +4008,7 @@
|
|||
"playWithAI": "Leiki tekoälyn kanssa nyt",
|
||||
"courseStartDesc": "Pangea Bot on valmis milloin tahansa!\n\n...mutta oppiminen on parempaa ystävien kanssa!",
|
||||
"@@locale": "fi",
|
||||
"@@last_modified": "2026-01-27 14:02:18.192045",
|
||||
"@@last_modified": "2026-01-28 13:25:57.438673",
|
||||
"@alwaysUse24HourFormat": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
|
|
@ -11557,5 +11557,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Mitä kieltä opit?",
|
||||
"searchLanguagesHint": "Etsi kohdekieliä",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Kysymyksiä? Olemme täällä auttamassa!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Jotain meni pieleen, ja teemme kovasti töitä sen korjaamiseksi. Tarkista myöhemmin uudelleen.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -2786,7 +2786,7 @@
|
|||
"selectAll": "Piliin lahat",
|
||||
"deselectAll": "Huwag piliin lahat",
|
||||
"@@locale": "fil",
|
||||
"@@last_modified": "2026-01-27 14:02:56.958154",
|
||||
"@@last_modified": "2026-01-28 13:26:32.216257",
|
||||
"@setCustomPermissionLevel": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
|
|
@ -11910,5 +11910,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Anong wika ang iyong pinag-aaralan?",
|
||||
"searchLanguagesHint": "Maghanap ng mga target na wika",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "May mga tanong? Nandito kami para tumulong!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "May nangyaring mali, at abala kami sa pag-aayos nito. Suriin muli mamaya.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"@@locale": "fr",
|
||||
"@@last_modified": "2026-01-27 14:03:27.361514",
|
||||
"@@last_modified": "2026-01-28 13:27:03.910294",
|
||||
"about": "À propos",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -11258,5 +11258,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Quelle langue apprenez-vous ?",
|
||||
"searchLanguagesHint": "Recherchez des langues cibles",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Des questions ? Nous sommes là pour vous aider !",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Quelque chose a mal tourné, et nous travaillons dur pour le réparer. Vérifiez à nouveau plus tard.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -4516,7 +4516,7 @@
|
|||
"playWithAI": "Imir le AI faoi láthair",
|
||||
"courseStartDesc": "Tá Bot Pangea réidh chun dul am ar bith!\n\n...ach is fearr foghlaim le cairde!",
|
||||
"@@locale": "ga",
|
||||
"@@last_modified": "2026-01-27 14:03:26.279031",
|
||||
"@@last_modified": "2026-01-28 13:27:02.604512",
|
||||
"@customReaction": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
|
|
@ -10932,5 +10932,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Cén teanga atá á foghlaim agat?",
|
||||
"searchLanguagesHint": "Cuardaigh teangacha sprioc",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Ceisteanna? Táimid anseo chun cabhrú!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Tharla rud éigin mícheart, agus táimid ag obair go dian chun é a shocrú. Seiceáil arís níos déanaí.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"@@locale": "gl",
|
||||
"@@last_modified": "2026-01-27 14:02:17.041137",
|
||||
"@@last_modified": "2026-01-28 13:25:56.077589",
|
||||
"about": "Acerca de",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -10931,5 +10931,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Que idioma estás aprendendo?",
|
||||
"searchLanguagesHint": "Busca idiomas de destino",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "¿Preguntas? Estamos aquí para axudar!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Algo saíu mal e estamos traballando duro para solucionalo. Comproba de novo máis tarde.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"@@last_modified": "2026-01-27 14:02:31.662677",
|
||||
"@@last_modified": "2026-01-28 13:26:06.755080",
|
||||
"about": "אודות",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -11983,5 +11983,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "איזו שפה אתה לומד?",
|
||||
"searchLanguagesHint": "חפש שפות יעד",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "שאלות? אנחנו כאן כדי לעזור!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "משהו השתבש, ואנחנו עובדים קשה על תיקון זה. בדוק שוב מאוחר יותר.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -4482,7 +4482,7 @@
|
|||
"playWithAI": "अभी के लिए एआई के साथ खेलें",
|
||||
"courseStartDesc": "पैंजिया बॉट कभी भी जाने के लिए तैयार है!\n\n...लेकिन दोस्तों के साथ सीखना बेहतर है!",
|
||||
"@@locale": "hi",
|
||||
"@@last_modified": "2026-01-27 14:03:20.292018",
|
||||
"@@last_modified": "2026-01-28 13:26:55.548010",
|
||||
"@alwaysUse24HourFormat": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
|
|
@ -12019,5 +12019,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "आप कौन सी भाषा सीख रहे हैं?",
|
||||
"searchLanguagesHint": "लक्षित भाषाएँ खोजें",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "प्रश्न? हम आपकी मदद के लिए यहाँ हैं!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "कुछ गलत हो गया है, और हम इसे ठीक करने में कड़ी मेहनत कर रहे हैं। बाद में फिर से जांचें।",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"@@locale": "hr",
|
||||
"@@last_modified": "2026-01-27 14:02:30.204264",
|
||||
"@@last_modified": "2026-01-28 13:26:05.722028",
|
||||
"about": "Informacije",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -11306,5 +11306,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Koji jezik učite?",
|
||||
"searchLanguagesHint": "Pretraži ciljne jezike",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Imate pitanja? Tu smo da pomognemo!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Nešto je pošlo po zlu i marljivo radimo na rješavanju problema. Provjerite ponovo kasnije.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"@@locale": "hu",
|
||||
"@@last_modified": "2026-01-27 14:02:22.464338",
|
||||
"@@last_modified": "2026-01-28 13:25:59.737331",
|
||||
"about": "Névjegy",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -10935,5 +10935,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Milyen nyelvet tanulsz?",
|
||||
"searchLanguagesHint": "Keresd a célnyelveket",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Kérdése van? Itt vagyunk, hogy segítsünk!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Valami hiba történt, és keményen dolgozunk a javításon. Kérlek, nézd meg később.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1957,7 +1957,7 @@
|
|||
"playWithAI": "Joca con le IA pro ora",
|
||||
"courseStartDesc": "Pangea Bot es preste a comenzar a qualunque momento!\n\n...ma apprender es melior con amicos!",
|
||||
"@@locale": "ia",
|
||||
"@@last_modified": "2026-01-27 14:02:33.390978",
|
||||
"@@last_modified": "2026-01-28 13:26:08.333302",
|
||||
"@alwaysUse24HourFormat": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
|
|
@ -12012,5 +12012,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Kia lingvo vi lernas?",
|
||||
"searchLanguagesHint": "Serĉu celajn lingvojn",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Kwestyon? Nou la pou ede!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "N'ayen a fau, e n'ayen a t'awen a t'awen a t'awen. T'awen a t'awen a t'awen.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"@@last_modified": "2026-01-27 14:02:23.466673",
|
||||
"@@last_modified": "2026-01-28 13:26:00.772325",
|
||||
"setAsCanonicalAlias": "Atur sebagai alias utama",
|
||||
"@setAsCanonicalAlias": {
|
||||
"type": "String",
|
||||
|
|
@ -10925,5 +10925,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Bahasa apa yang Anda pelajari?",
|
||||
"searchLanguagesHint": "Cari bahasa target",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Pertanyaan? Kami di sini untuk membantu!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Ada yang tidak beres, dan kami sedang bekerja keras untuk memperbaikinya. Periksa lagi nanti.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -4371,7 +4371,7 @@
|
|||
"playWithAI": "Joca con AI pro ora",
|
||||
"courseStartDesc": "Pangea Bot es preste a partir a qualunque momento!\n\n...ma apprender es melior con amicos!",
|
||||
"@@locale": "ie",
|
||||
"@@last_modified": "2026-01-27 14:02:28.999815",
|
||||
"@@last_modified": "2026-01-28 13:26:04.525215",
|
||||
"@alwaysUse24HourFormat": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
|
|
@ -11908,5 +11908,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Cén teanga atá á foghlaim agat?",
|
||||
"searchLanguagesHint": "Cuardaigh teangacha sprioc",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Ceisteanna? Táimid anseo chun cabhrú!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Níl aon rud ag dul i gceart, agus táimid ag obair go dian chun é a shocrú. Seiceáil arís níos déanaí.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"@@last_modified": "2026-01-27 14:02:43.998266",
|
||||
"@@last_modified": "2026-01-28 13:26:18.238331",
|
||||
"about": "Informazioni",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -10937,5 +10937,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Quale lingua stai imparando?",
|
||||
"searchLanguagesHint": "Cerca lingue target",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Domande? Siamo qui per aiutarti!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Qualcosa è andato storto e stiamo lavorando duramente per risolverlo. Controlla di nuovo più tardi.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"@@locale": "ja",
|
||||
"@@last_modified": "2026-01-27 14:03:18.964652",
|
||||
"@@last_modified": "2026-01-28 13:26:54.166864",
|
||||
"about": "このアプリについて",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -11724,5 +11724,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "どの言語を学んでいますか?",
|
||||
"searchLanguagesHint": "ターゲット言語を検索",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "質問がありますか?私たちはお手伝いします!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "何かがうまくいかなかったため、私たちは修正作業に取り組んでいます。後で再度確認してください。",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -2593,7 +2593,7 @@
|
|||
"playWithAI": "ამ დროისთვის ითამაშეთ AI-თან",
|
||||
"courseStartDesc": "Pangea Bot მზადაა ნებისმიერ დროს გასასვლელად!\n\n...მაგრამ სწავლა უკეთესია მეგობრებთან ერთად!",
|
||||
"@@locale": "ka",
|
||||
"@@last_modified": "2026-01-27 14:03:23.702338",
|
||||
"@@last_modified": "2026-01-28 13:27:00.130109",
|
||||
"@alwaysUse24HourFormat": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
|
|
@ -11964,5 +11964,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "რომელი ენა სწავლობთ?",
|
||||
"searchLanguagesHint": "ძებნა მიზნობრივი ენების",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "კითხვები? ჩვენ აქ ვართ, რომ დაგეხმაროთ!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "რამე არასწორად მოხდა, და ჩვენ აქტიურად ვმუშაობთ ამის გამოსასწორებლად. შეამოწმეთ მოგვიანებით.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"@@last_modified": "2026-01-27 14:02:13.792128",
|
||||
"@@last_modified": "2026-01-28 13:25:53.869320",
|
||||
"about": "소개",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -11042,5 +11042,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "어떤 언어를 배우고 있나요?",
|
||||
"searchLanguagesHint": "목표 언어 검색",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "질문이 있으신가요? 저희가 도와드리겠습니다!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "문제가 발생했으며, 우리는 이를 해결하기 위해 열심히 작업하고 있습니다. 나중에 다시 확인해 주세요.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -3860,7 +3860,7 @@
|
|||
"playWithAI": "Žaiskite su dirbtiniu intelektu dabar",
|
||||
"courseStartDesc": "Pangea botas pasiruošęs bet kada pradėti!\n\n...bet mokymasis yra geresnis su draugais!",
|
||||
"@@locale": "lt",
|
||||
"@@last_modified": "2026-01-27 14:03:05.189233",
|
||||
"@@last_modified": "2026-01-28 13:26:39.413825",
|
||||
"@alwaysUse24HourFormat": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
|
|
@ -11739,5 +11739,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Kokią kalbą mokotės?",
|
||||
"searchLanguagesHint": "Ieškoti tikslo kalbų",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Klausimai? Mes čia, kad padėtume!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Kažkas nepavyko, ir mes sunkiai dirbame, kad tai išspręstume. Patikrinkite vėliau.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -4482,7 +4482,7 @@
|
|||
"playWithAI": "Tagad spēlējiet ar AI",
|
||||
"courseStartDesc": "Pangea bots ir gatavs jebkurā laikā!\n\n...bet mācīties ir labāk ar draugiem!",
|
||||
"@@locale": "lv",
|
||||
"@@last_modified": "2026-01-27 14:02:58.530525",
|
||||
"@@last_modified": "2026-01-28 13:26:33.690801",
|
||||
"analyticsInactiveTitle": "Pieprasījumi neaktīviem lietotājiem nevar tikt nosūtīti",
|
||||
"analyticsInactiveDesc": "Neaktīvi lietotāji, kuri nav pieteikušies kopš šīs funkcijas ieviešanas, neredzēs jūsu pieprasījumu.\n\nPieprasījuma poga parādīsies, kad viņi atgriezīsies. Jūs varat atkārtoti nosūtīt pieprasījumu vēlāk, noklikšķinot uz pieprasījuma pogas viņu vārdā, kad tā būs pieejama.",
|
||||
"accessRequestedTitle": "Pieprasījums piekļūt analītikai",
|
||||
|
|
@ -10920,5 +10920,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Kuru valodu tu mācies?",
|
||||
"searchLanguagesHint": "Meklēt mērķa valodas",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Jautājumi? Mēs esam šeit, lai palīdzētu!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Kaut kas nogāja greizi, un mēs smagi strādājam, lai to labotu. Pārbaudiet vēlāk.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"@@last_modified": "2026-01-27 14:02:48.767207",
|
||||
"@@last_modified": "2026-01-28 13:26:21.535919",
|
||||
"about": "Om",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -12027,5 +12027,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Hvilket språk lærer du?",
|
||||
"searchLanguagesHint": "Søk etter målspråk",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Spørsmål? Vi er her for å hjelpe!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Noe gikk galt, og vi jobber hardt med å fikse det. Sjekk igjen senere.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"@@last_modified": "2026-01-27 14:03:08.964846",
|
||||
"@@last_modified": "2026-01-28 13:26:43.074504",
|
||||
"about": "Over ons",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -10934,5 +10934,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Welke taal ben je aan het leren?",
|
||||
"searchLanguagesHint": "Zoek doeltalen",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Vragen? We zijn hier om te helpen!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Er is iets misgegaan en we zijn hard aan het werk om het op te lossen. Kijk later nog eens.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"@@locale": "pl",
|
||||
"@@last_modified": "2026-01-27 14:03:14.463461",
|
||||
"@@last_modified": "2026-01-28 13:26:50.545730",
|
||||
"about": "O aplikacji",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -10932,5 +10932,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Jakiego języka się uczysz?",
|
||||
"searchLanguagesHint": "Szukaj języków docelowych",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Pytania? Jesteśmy tutaj, aby pomóc!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Coś poszło nie tak, a my ciężko pracujemy nad naprawą. Sprawdź ponownie później.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"@@last_modified": "2026-01-27 14:02:37.702228",
|
||||
"@@last_modified": "2026-01-28 13:26:11.994676",
|
||||
"copiedToClipboard": "Copiada para a área de transferência",
|
||||
"@copiedToClipboard": {
|
||||
"type": "String",
|
||||
|
|
@ -12034,5 +12034,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Qual idioma você está aprendendo?",
|
||||
"searchLanguagesHint": "Pesquise idiomas-alvo",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Dúvidas? Estamos aqui para ajudar!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Algo deu errado, e estamos trabalhando arduamente para corrigir isso. Verifique novamente mais tarde.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"@@last_modified": "2026-01-27 14:02:34.316481",
|
||||
"@@last_modified": "2026-01-28 13:26:09.473589",
|
||||
"about": "Sobre",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -11292,5 +11292,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Qual idioma você está aprendendo?",
|
||||
"searchLanguagesHint": "Pesquise idiomas-alvo",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Dúvidas? Estamos aqui para ajudar!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Algo deu errado, e estamos trabalhando duro para consertar. Verifique novamente mais tarde.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -3330,7 +3330,7 @@
|
|||
"selectAll": "Selecionar tudo",
|
||||
"deselectAll": "Desmarcar tudo",
|
||||
"@@locale": "pt_PT",
|
||||
"@@last_modified": "2026-01-27 14:02:54.292550",
|
||||
"@@last_modified": "2026-01-28 13:26:26.329625",
|
||||
"@alwaysUse24HourFormat": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
|
|
@ -11963,5 +11963,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Qual idioma você está aprendendo?",
|
||||
"searchLanguagesHint": "Pesquise idiomas-alvo",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Dúvidas? Estamos aqui para ajudar!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Algo deu errado, e estamos trabalhando arduamente para corrigir isso. Verifique novamente mais tarde.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"@@last_modified": "2026-01-27 14:02:25.277330",
|
||||
"@@last_modified": "2026-01-28 13:26:01.958335",
|
||||
"about": "Despre",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -11669,5 +11669,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Ce limbă înveți?",
|
||||
"searchLanguagesHint": "Caută limbi țintă",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Întrebări? Suntem aici să ajutăm!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Ceva a mers prost și lucrăm din greu pentru a remedia problema. Verifică din nou mai târziu.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"@@locale": "ru",
|
||||
"@@last_modified": "2026-01-27 14:03:22.446889",
|
||||
"@@last_modified": "2026-01-28 13:26:58.889837",
|
||||
"about": "О проекте",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -11042,5 +11042,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Какой язык вы изучаете?",
|
||||
"searchLanguagesHint": "Поиск целевых языков",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Вопросы? Мы здесь, чтобы помочь!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Что-то пошло не так, и мы усердно работаем над исправлением. Проверьте позже.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"@@locale": "sk",
|
||||
"@@last_modified": "2026-01-27 14:02:27.134602",
|
||||
"@@last_modified": "2026-01-28 13:26:03.157059",
|
||||
"about": "O aplikácii",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -12018,5 +12018,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Aký jazyk sa učíte?",
|
||||
"searchLanguagesHint": "Hľadajte cieľové jazyky",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Otázky? Sme tu, aby sme pomohli!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Niečo sa pokazilo a my na tom tvrdo pracujeme, aby sme to opravili. Skontrolujte to neskôr.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -2463,7 +2463,7 @@
|
|||
"playWithAI": "Za zdaj igrajte z AI-jem",
|
||||
"courseStartDesc": "Pangea Bot je pripravljen kadarkoli!\n\n...ampak je bolje učiti se s prijatelji!",
|
||||
"@@locale": "sl",
|
||||
"@@last_modified": "2026-01-27 14:02:41.198883",
|
||||
"@@last_modified": "2026-01-28 13:26:15.797667",
|
||||
"@alwaysUse24HourFormat": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
|
|
@ -12015,5 +12015,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Katero jezika se učiš?",
|
||||
"searchLanguagesHint": "Išči ciljne jezike",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Vprašanja? Tu smo, da pomagamo!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Nekaj je šlo narobe in trdo delamo na tem, da to popravimo. Preverite znova kasneje.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"@@last_modified": "2026-01-27 14:03:24.807506",
|
||||
"@@last_modified": "2026-01-28 13:27:01.338972",
|
||||
"about": "О програму",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -12036,5 +12036,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Koji jezik učite?",
|
||||
"searchLanguagesHint": "Pretraži ciljne jezike",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Pitanja? Tu smo da pomognemo!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Nešto je pošlo po zlu, i mi marljivo radimo na rešenju. Proverite ponovo kasnije.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"@@last_modified": "2026-01-27 14:03:15.575050",
|
||||
"@@last_modified": "2026-01-28 13:26:51.814505",
|
||||
"about": "Om",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -11412,5 +11412,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Vilket språk lär du dig?",
|
||||
"searchLanguagesHint": "Sök efter målspråk",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Frågor? Vi är här för att hjälpa till!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Något gick fel, och vi arbetar hårt för att åtgärda det. Kolla igen senare.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"@@last_modified": "2026-01-27 14:03:07.633079",
|
||||
"@@last_modified": "2026-01-28 13:26:42.100203",
|
||||
"acceptedTheInvitation": "👍 {username} அழைப்பை ஏற்றுக்கொண்டது",
|
||||
"@acceptedTheInvitation": {
|
||||
"type": "String",
|
||||
|
|
@ -11158,5 +11158,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "நீங்கள் எது மொழி கற்றுக்கொள்கிறீர்கள்?",
|
||||
"searchLanguagesHint": "இலக்கு மொழிகளை தேடுங்கள்",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "கேள்விகள்? நாங்கள் உதவ இங்கே இருக்கிறோம்!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "எதோ தவறு ஏற்பட்டது, அதை சரிசெய்ய நாங்கள் கடுமையாக வேலை செய்கிறோம். பின்னர் மீண்டும் சரிபார்க்கவும்.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1919,7 +1919,7 @@
|
|||
"playWithAI": "ఇప్పుడే AI తో ఆడండి",
|
||||
"courseStartDesc": "పాంజియా బాట్ ఎప్పుడైనా సిద్ధంగా ఉంటుంది!\n\n...కానీ స్నేహితులతో నేర్చుకోవడం మెరుగైనది!",
|
||||
"@@locale": "te",
|
||||
"@@last_modified": "2026-01-27 14:03:02.156703",
|
||||
"@@last_modified": "2026-01-28 13:26:38.102435",
|
||||
"@setCustomPermissionLevel": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
|
|
@ -12023,5 +12023,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "మీరు ఏ భాష నేర్చుకుంటున్నారు?",
|
||||
"searchLanguagesHint": "లక్ష్య భాషలను శోధించండి",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "ప్రశ్నలు? మేము మీకు సహాయం చేయడానికి ఇక్కడ ఉన్నాము!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "ఏదో తప్పు జరిగింది, మరియు మేము దీన్ని సరిదిద్దడానికి కష్టపడుతున్నాము. తర్వాత మళ్లీ తనిఖీ చేయండి.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -4455,7 +4455,7 @@
|
|||
"playWithAI": "เล่นกับ AI ชั่วคราว",
|
||||
"courseStartDesc": "Pangea Bot พร้อมที่จะเริ่มต้นได้ทุกเมื่อ!\n\n...แต่การเรียนรู้ดีกว่ากับเพื่อน!",
|
||||
"@@locale": "th",
|
||||
"@@last_modified": "2026-01-27 14:02:53.265962",
|
||||
"@@last_modified": "2026-01-28 13:26:25.241346",
|
||||
"@alwaysUse24HourFormat": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
|
|
@ -11992,5 +11992,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "คุณกำลังเรียนภาษาอะไรอยู่?",
|
||||
"searchLanguagesHint": "ค้นหาภาษาที่ต้องการ",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "มีคำถามไหม? เราพร้อมที่จะช่วยเหลือ!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "มีบางอย่างผิดพลาด และเรากำลังทำงานอย่างหนักเพื่อแก้ไข ตรวจสอบอีกครั้งในภายหลัง.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"@@locale": "tr",
|
||||
"@@last_modified": "2026-01-27 14:03:00.927147",
|
||||
"@@last_modified": "2026-01-28 13:26:36.845268",
|
||||
"about": "Hakkında",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -11156,5 +11156,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Hangi dili öğreniyorsunuz?",
|
||||
"searchLanguagesHint": "Hedef dilleri arayın",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Sorular mı? Yardımcı olmaya buradayız!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Bir şeyler yanlış gitti ve biz bunu düzeltmek için yoğun bir şekilde çalışıyoruz. Lütfen daha sonra tekrar kontrol edin.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"@@locale": "uk",
|
||||
"@@last_modified": "2026-01-27 14:02:45.151936",
|
||||
"@@last_modified": "2026-01-28 13:26:19.427065",
|
||||
"about": "Про застосунок",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -10928,5 +10928,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Яку мову ви вивчаєте?",
|
||||
"searchLanguagesHint": "Шукати цільові мови",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Питання? Ми тут, щоб допомогти!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Щось пішло не так, і ми наполегливо працюємо над виправленням. Перевірте пізніше.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"@@last_modified": "2026-01-27 14:03:06.468185",
|
||||
"@@last_modified": "2026-01-28 13:26:40.659203",
|
||||
"about": "Giới thiệu",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -6504,5 +6504,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "Bạn đang học ngôn ngữ nào?",
|
||||
"searchLanguagesHint": "Tìm kiếm ngôn ngữ mục tiêu",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "Câu hỏi? Chúng tôi ở đây để giúp đỡ!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "Đã xảy ra sự cố, và chúng tôi đang nỗ lực khắc phục. Vui lòng kiểm tra lại sau.",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1855,7 +1855,7 @@
|
|||
"selectAll": "全選",
|
||||
"deselectAll": "取消全選",
|
||||
"@@locale": "yue",
|
||||
"@@last_modified": "2026-01-27 14:02:42.486712",
|
||||
"@@last_modified": "2026-01-28 13:26:16.825978",
|
||||
"@ignoreUser": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
|
|
@ -12025,5 +12025,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "你正在學習什麼語言?",
|
||||
"searchLanguagesHint": "搜尋目標語言",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "有問題嗎?我們在這裡幫助你!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "發生了一些問題,我們正在努力修復。稍後再檢查。",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"@@locale": "zh",
|
||||
"@@last_modified": "2026-01-27 14:03:10.872296",
|
||||
"@@last_modified": "2026-01-28 13:26:45.843538",
|
||||
"about": "关于",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -10925,5 +10925,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "你正在学习什么语言?",
|
||||
"searchLanguagesHint": "搜索目标语言",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "有问题吗?我们在这里帮助您!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "出现了一些问题,我们正在努力修复。请稍后再检查。",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"@@last_modified": "2026-01-27 14:02:55.518251",
|
||||
"@@last_modified": "2026-01-28 13:26:27.558260",
|
||||
"about": "關於",
|
||||
"@about": {
|
||||
"type": "String",
|
||||
|
|
@ -10932,5 +10932,25 @@
|
|||
"@newCourseAccess": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"onboardingLanguagesTitle": "你正在學習什麼語言?",
|
||||
"searchLanguagesHint": "搜尋目標語言",
|
||||
"@onboardingLanguagesTitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"@searchLanguagesHint": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supportSubtitle": "有問題嗎?我們在這裡幫助您!",
|
||||
"@supportSubtitle": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"courseLoadingError": "發生了一些問題,我們正在努力修復。稍後再檢查。",
|
||||
"@courseLoadingError": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -2015,18 +2015,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
bool get _isToolbarOpen =>
|
||||
MatrixState.pAnyState.isOverlayOpen(RegExp(r'^message_toolbar_overlay$'));
|
||||
|
||||
bool showMessageShimmer(Event event) {
|
||||
if (event.type != EventTypes.Message) return false;
|
||||
if (!(event.eventId == buttonEventID)) return false;
|
||||
if (event.messageType == MessageTypes.Text) {
|
||||
return !InstructionsEnum.clickTextMessages.isToggledOff;
|
||||
}
|
||||
if (event.messageType == MessageTypes.Audio) {
|
||||
return !InstructionsEnum.clickAudioMessages.isToggledOff;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void showToolbar(
|
||||
Event event, {
|
||||
PangeaMessageEvent? pangeaMessageEvent,
|
||||
|
|
@ -2059,14 +2047,8 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
);
|
||||
|
||||
// you've clicked a message so lets turn this off
|
||||
InstructionsEnum.clickMessage.setToggledOff(true);
|
||||
if (event.messageType == MessageTypes.Text &&
|
||||
!InstructionsEnum.clickTextMessages.isToggledOff) {
|
||||
InstructionsEnum.clickTextMessages.setToggledOff(true);
|
||||
}
|
||||
if (event.messageType == MessageTypes.Audio &&
|
||||
!InstructionsEnum.clickAudioMessages.isToggledOff) {
|
||||
InstructionsEnum.clickAudioMessages.setToggledOff(true);
|
||||
if (!InstructionsEnum.clickMessage.isToggledOff) {
|
||||
InstructionsEnum.clickMessage.setToggledOff(true);
|
||||
}
|
||||
|
||||
if (!kIsWeb) {
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/shimmer_background.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/layout/reading_assistance_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/message_practice/message_practice_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/message_practice/token_practice_button.dart';
|
||||
|
|
@ -446,6 +448,12 @@ class HtmlMessage extends StatelessWidget {
|
|||
: false;
|
||||
|
||||
final isNew = token != null && newTokens.contains(token.text);
|
||||
final isFirstNewToken = isNew &&
|
||||
controller.buttonEventID == event.eventId &&
|
||||
newTokens.first == token.text;
|
||||
final showShimmer =
|
||||
!InstructionsEnum.shimmerNewToken.isToggledOff && isFirstNewToken;
|
||||
|
||||
final tokenWidth = renderer.tokenTextWidthForContainer(
|
||||
node.text,
|
||||
Theme.of(context).colorScheme.primary.withAlpha(200),
|
||||
|
|
@ -500,19 +508,25 @@ class HtmlMessage extends StatelessWidget {
|
|||
: null,
|
||||
child: HoverBuilder(
|
||||
builder: (context, hovered) {
|
||||
return UnderlineText(
|
||||
text: node.text.trim(),
|
||||
style: existingStyle,
|
||||
linkStyle: linkStyle,
|
||||
textDirection: pangeaMessageEvent?.textDirection,
|
||||
underlineColor: TokenRenderingUtil.underlineColor(
|
||||
underlineColor,
|
||||
selected: selected,
|
||||
highlighted: highlighted,
|
||||
isNew: isNew,
|
||||
practiceMode: readingAssistanceMode ==
|
||||
ReadingAssistanceMode.practiceMode,
|
||||
hovered: hovered,
|
||||
return ShimmerBackground(
|
||||
enabled: showShimmer,
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
child: UnderlineText(
|
||||
text: node.text.trim(),
|
||||
style: existingStyle,
|
||||
linkStyle: linkStyle,
|
||||
textDirection:
|
||||
pangeaMessageEvent?.textDirection,
|
||||
underlineColor:
|
||||
TokenRenderingUtil.underlineColor(
|
||||
underlineColor,
|
||||
selected: selected,
|
||||
highlighted: highlighted,
|
||||
isNew: isNew,
|
||||
practiceMode: readingAssistanceMode ==
|
||||
ReadingAssistanceMode.practiceMode,
|
||||
hovered: hovered,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
|||
import 'package:fluffychat/pangea/bot/widgets/bot_settings_language_icon.dart';
|
||||
import 'package:fluffychat/pangea/chat/extensions/custom_room_display_extension.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/shimmer_background.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
|
|
@ -603,228 +602,223 @@ class Message extends StatelessWidget {
|
|||
child: ValueListenableBuilder(
|
||||
valueListenable: controller
|
||||
.depressMessageButton,
|
||||
// #Pangea
|
||||
child: ShimmerBackground(
|
||||
enabled: controller
|
||||
.showMessageShimmer(
|
||||
event,
|
||||
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: noBubble
|
||||
? Colors.transparent
|
||||
: color,
|
||||
borderRadius:
|
||||
borderRadius,
|
||||
),
|
||||
// Pangea#
|
||||
child: Container(
|
||||
decoration:
|
||||
BoxDecoration(
|
||||
color: noBubble
|
||||
? Colors
|
||||
.transparent
|
||||
: color,
|
||||
borderRadius:
|
||||
borderRadius,
|
||||
),
|
||||
clipBehavior:
|
||||
Clip.antiAlias,
|
||||
// #Pangea
|
||||
child:
|
||||
CompositedTransformTarget(
|
||||
link: MatrixState
|
||||
clipBehavior:
|
||||
Clip.antiAlias,
|
||||
// #Pangea
|
||||
child:
|
||||
CompositedTransformTarget(
|
||||
link: MatrixState
|
||||
.pAnyState
|
||||
.layerLinkAndKey(
|
||||
event.eventId,
|
||||
)
|
||||
.link,
|
||||
// child: BubbleBackground(
|
||||
// colors: colors,
|
||||
// ignore: noBubble || !ownMessage,
|
||||
// scrollController: scrollController,
|
||||
// Pangea#
|
||||
child: Container(
|
||||
// #Pangea
|
||||
key: MatrixState
|
||||
.pAnyState
|
||||
.layerLinkAndKey(
|
||||
event.eventId,
|
||||
)
|
||||
.link,
|
||||
// child: BubbleBackground(
|
||||
// colors: colors,
|
||||
// ignore: noBubble || !ownMessage,
|
||||
// scrollController: scrollController,
|
||||
.key,
|
||||
// Pangea#
|
||||
child: Container(
|
||||
// #Pangea
|
||||
key: MatrixState
|
||||
.pAnyState
|
||||
.layerLinkAndKey(
|
||||
event.eventId,
|
||||
)
|
||||
.key,
|
||||
// Pangea#
|
||||
decoration:
|
||||
BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius
|
||||
.circular(
|
||||
AppConfig
|
||||
.borderRadius,
|
||||
),
|
||||
decoration:
|
||||
BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius
|
||||
.circular(
|
||||
AppConfig
|
||||
.borderRadius,
|
||||
),
|
||||
constraints:
|
||||
const BoxConstraints(
|
||||
maxWidth: FluffyThemes
|
||||
.columnWidth *
|
||||
1.5,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize:
|
||||
MainAxisSize
|
||||
.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment
|
||||
.start,
|
||||
children: <Widget>[
|
||||
if ({
|
||||
RelationshipTypes
|
||||
.reply,
|
||||
RelationshipTypes
|
||||
.thread,
|
||||
}.contains(
|
||||
event
|
||||
.relationshipType,
|
||||
))
|
||||
FutureBuilder<
|
||||
Event?>(
|
||||
future: event
|
||||
.getReplyEvent(
|
||||
timeline,
|
||||
),
|
||||
builder: (
|
||||
BuildContext
|
||||
context,
|
||||
snapshot,
|
||||
) {
|
||||
final replyEvent = snapshot
|
||||
.hasData
|
||||
? snapshot
|
||||
.data!
|
||||
: Event(
|
||||
eventId: event.relationshipEventId!,
|
||||
content: {
|
||||
'msgtype': 'm.text',
|
||||
'body': '...',
|
||||
},
|
||||
// #Pangea
|
||||
// senderId: event
|
||||
// .senderId,
|
||||
senderId: "",
|
||||
// Pangea#
|
||||
type: 'm.room.message',
|
||||
room: event.room,
|
||||
status: EventStatus.sent,
|
||||
originServerTs: DateTime.now(),
|
||||
);
|
||||
return Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(
|
||||
left:
|
||||
16,
|
||||
right:
|
||||
16,
|
||||
top:
|
||||
8,
|
||||
),
|
||||
),
|
||||
constraints:
|
||||
const BoxConstraints(
|
||||
maxWidth: FluffyThemes
|
||||
.columnWidth *
|
||||
1.5,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize:
|
||||
MainAxisSize
|
||||
.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment
|
||||
.start,
|
||||
children: <Widget>[
|
||||
if ({
|
||||
RelationshipTypes
|
||||
.reply,
|
||||
RelationshipTypes
|
||||
.thread,
|
||||
}.contains(
|
||||
event
|
||||
.relationshipType,
|
||||
))
|
||||
FutureBuilder<
|
||||
Event?>(
|
||||
future: event
|
||||
.getReplyEvent(
|
||||
timeline,
|
||||
),
|
||||
builder: (
|
||||
BuildContext
|
||||
context,
|
||||
snapshot,
|
||||
) {
|
||||
final replyEvent = snapshot
|
||||
.hasData
|
||||
? snapshot
|
||||
.data!
|
||||
: Event(
|
||||
eventId:
|
||||
event.relationshipEventId!,
|
||||
content: {
|
||||
'msgtype': 'm.text',
|
||||
'body': '...',
|
||||
},
|
||||
// #Pangea
|
||||
// senderId: event
|
||||
// .senderId,
|
||||
senderId:
|
||||
"",
|
||||
// Pangea#
|
||||
type:
|
||||
'm.room.message',
|
||||
room:
|
||||
event.room,
|
||||
status:
|
||||
EventStatus.sent,
|
||||
originServerTs:
|
||||
DateTime.now(),
|
||||
);
|
||||
return Padding(
|
||||
padding:
|
||||
const EdgeInsets
|
||||
.only(
|
||||
left:
|
||||
16,
|
||||
right:
|
||||
16,
|
||||
top: 8,
|
||||
),
|
||||
child:
|
||||
Material(
|
||||
color: Colors
|
||||
.transparent,
|
||||
borderRadius:
|
||||
ReplyContent.borderRadius,
|
||||
child:
|
||||
Material(
|
||||
color:
|
||||
Colors.transparent,
|
||||
InkWell(
|
||||
borderRadius:
|
||||
ReplyContent.borderRadius,
|
||||
onTap: () =>
|
||||
scrollToEventId(
|
||||
replyEvent.eventId,
|
||||
),
|
||||
child:
|
||||
InkWell(
|
||||
borderRadius:
|
||||
ReplyContent.borderRadius,
|
||||
onTap: () =>
|
||||
scrollToEventId(
|
||||
replyEvent.eventId,
|
||||
),
|
||||
AbsorbPointer(
|
||||
child:
|
||||
AbsorbPointer(
|
||||
child: ReplyContent(
|
||||
replyEvent,
|
||||
ownMessage: ownMessage,
|
||||
timeline: timeline,
|
||||
),
|
||||
ReplyContent(
|
||||
replyEvent,
|
||||
ownMessage: ownMessage,
|
||||
timeline: timeline,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
MessageContent(
|
||||
displayEvent,
|
||||
textColor:
|
||||
textColor,
|
||||
linkColor:
|
||||
linkColor,
|
||||
onInfoTab:
|
||||
onInfoTab,
|
||||
borderRadius:
|
||||
borderRadius,
|
||||
timeline:
|
||||
timeline,
|
||||
selected:
|
||||
selected,
|
||||
// #Pangea
|
||||
pangeaMessageEvent:
|
||||
pangeaMessageEvent,
|
||||
controller:
|
||||
controller,
|
||||
nextEvent:
|
||||
nextEvent,
|
||||
prevEvent:
|
||||
previousEvent,
|
||||
// Pangea#
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (event
|
||||
.hasAggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes
|
||||
.edit,
|
||||
))
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets
|
||||
.only(
|
||||
bottom:
|
||||
8.0,
|
||||
left:
|
||||
16.0,
|
||||
right:
|
||||
16.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize:
|
||||
MainAxisSize
|
||||
.min,
|
||||
spacing:
|
||||
4.0,
|
||||
children: [
|
||||
Icon(
|
||||
Icons
|
||||
.edit_outlined,
|
||||
MessageContent(
|
||||
displayEvent,
|
||||
textColor:
|
||||
textColor,
|
||||
linkColor:
|
||||
linkColor,
|
||||
onInfoTab:
|
||||
onInfoTab,
|
||||
borderRadius:
|
||||
borderRadius,
|
||||
timeline:
|
||||
timeline,
|
||||
selected:
|
||||
selected,
|
||||
// #Pangea
|
||||
pangeaMessageEvent:
|
||||
pangeaMessageEvent,
|
||||
controller:
|
||||
controller,
|
||||
nextEvent:
|
||||
nextEvent,
|
||||
prevEvent:
|
||||
previousEvent,
|
||||
// Pangea#
|
||||
),
|
||||
if (event
|
||||
.hasAggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes
|
||||
.edit,
|
||||
))
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets
|
||||
.only(
|
||||
bottom: 8.0,
|
||||
left: 16.0,
|
||||
right: 16.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize:
|
||||
MainAxisSize
|
||||
.min,
|
||||
spacing:
|
||||
4.0,
|
||||
children: [
|
||||
Icon(
|
||||
Icons
|
||||
.edit_outlined,
|
||||
color: textColor
|
||||
.withAlpha(
|
||||
164,
|
||||
),
|
||||
size:
|
||||
14,
|
||||
),
|
||||
Text(
|
||||
displayEvent
|
||||
.originServerTs
|
||||
.localizedTimeShort(
|
||||
context,
|
||||
),
|
||||
style:
|
||||
TextStyle(
|
||||
color:
|
||||
textColor.withAlpha(
|
||||
164,
|
||||
),
|
||||
size:
|
||||
14,
|
||||
fontSize:
|
||||
11,
|
||||
),
|
||||
Text(
|
||||
displayEvent
|
||||
.originServerTs
|
||||
.localizedTimeShort(
|
||||
context,
|
||||
),
|
||||
style:
|
||||
TextStyle(
|
||||
color:
|
||||
textColor.withAlpha(
|
||||
164,
|
||||
),
|
||||
fontSize:
|
||||
11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -380,6 +380,7 @@ class ChatListViewBody extends StatelessWidget {
|
|||
.setToggledOff(true),
|
||||
),
|
||||
title: Text(L10n.of(context).chatWithSupport),
|
||||
subtitle: Text(L10n.of(context).supportSubtitle),
|
||||
onTap: () async {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/new_private_chat/new_private_chat_view.dart';
|
||||
import 'package:fluffychat/pages/new_private_chat/qr_scanner_modal.dart';
|
||||
import 'package:fluffychat/pangea/user/user_search_extension.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
|
|
@ -52,8 +53,11 @@ class NewPrivateChatController extends State<NewPrivateChat> {
|
|||
}
|
||||
|
||||
Future<List<Profile>> _searchUser(String searchTerm) async {
|
||||
final result =
|
||||
await Matrix.of(context).client.searchUserDirectory(searchTerm);
|
||||
// #Pangea
|
||||
// final result =
|
||||
// await Matrix.of(context).client.searchUserDirectory(searchTerm);
|
||||
final result = await Matrix.of(context).client.searchUser(searchTerm);
|
||||
// Pangea#
|
||||
final profiles = result.results;
|
||||
|
||||
if (searchTerm.isValidMatrixId &&
|
||||
|
|
|
|||
|
|
@ -19,12 +19,26 @@ class SpaceCodeOnboardingView extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return PangeaLoginScaffold(
|
||||
customAppBar: AppBar(
|
||||
leading: BackButton(
|
||||
onPressed: () => pLogoutAction(
|
||||
context,
|
||||
bypassWarning: true,
|
||||
title: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 450,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
BackButton(
|
||||
onPressed: () => pLogoutAction(
|
||||
context,
|
||||
bypassWarning: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 40.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
showAppName: false,
|
||||
mainAssetUrl: controller.profile?.avatarUrl,
|
||||
|
|
|
|||
|
|
@ -353,7 +353,8 @@ extension ActivityRoomExtension on Room {
|
|||
bool get isActivitySession =>
|
||||
(roomType?.startsWith(PangeaRoomTypes.activitySession) == true ||
|
||||
activityPlan != null) &&
|
||||
activityPlan?.isDeprecatedModel == false;
|
||||
activityPlan?.isDeprecatedModel == false &&
|
||||
activityPlan?.activityId != null;
|
||||
|
||||
String? get activityId {
|
||||
if (!isActivitySession) return null;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart
|
|||
import 'package:fluffychat/pangea/activity_summary/activity_summary_model.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
||||
import 'package:fluffychat/pangea/languages/p_language_store.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class ActivityFinishedStatusMessage extends StatelessWidget {
|
||||
|
|
@ -28,10 +29,17 @@ class ActivityFinishedStatusMessage extends StatelessWidget {
|
|||
|
||||
Future<void> _archiveToAnalytics() async {
|
||||
try {
|
||||
final activityPlan = controller.room.activityPlan;
|
||||
if (activityPlan == null) {
|
||||
throw Exception("No activity plan found for room");
|
||||
}
|
||||
|
||||
final lang = activityPlan.req.targetLanguage.split("-").first;
|
||||
final langModel = PLanguageStore.byLangCode(lang)!;
|
||||
await controller.room.archiveActivity();
|
||||
await MatrixState
|
||||
.pangeaController.matrixState.analyticsDataService.updateService
|
||||
.sendActivityAnalytics(controller.room.id);
|
||||
.sendActivityAnalytics(controller.room.id, langModel);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activ
|
|||
import 'package:fluffychat/pangea/activity_sessions/activity_session_start/bot_join_error_dialog.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/room_summary_extension.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/course_activities/activity_summaries_provider.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/course_activities/course_activity_repo.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/course_activities/course_activity_translation_request.dart';
|
||||
|
|
@ -191,7 +192,7 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage>
|
|||
|
||||
final availableRoles = activity!.roles;
|
||||
final assignedRoles = activityRoom?.assignedRoles ??
|
||||
roomSummaries?[widget.roomId]?.activityRoles.roles ??
|
||||
roomSummaries?[widget.roomId]?.activityRoles?.roles ??
|
||||
{};
|
||||
final unassignedIds = availableRoles.keys
|
||||
.where((id) => !assignedRoles.containsKey(id))
|
||||
|
|
@ -293,8 +294,17 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage>
|
|||
);
|
||||
}
|
||||
await Future.wait(futures);
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
error = e;
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"activityId": widget.activityId,
|
||||
"roomId": widget.roomId,
|
||||
"parentId": widget.parentId,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => loading = false);
|
||||
|
|
|
|||
|
|
@ -518,7 +518,7 @@ class _ActivityStatuses extends StatelessWidget {
|
|||
// room (like the bot). Otherwise, show only joined users with roles
|
||||
Map<String, ActivityRoleModel> activityRoles =
|
||||
status == ActivitySummaryStatus.completed
|
||||
? e.value.activityRoles.roles
|
||||
? (e.value.activityRoles?.roles ?? {})
|
||||
: e.value.joinedUsersWithRoles;
|
||||
|
||||
// If the user is in the activity room and it's not completed, use the room's
|
||||
|
|
@ -530,7 +530,7 @@ class _ActivityStatuses extends StatelessWidget {
|
|||
|
||||
return ListTile(
|
||||
title: OpenRolesIndicator(
|
||||
roles: activityPlan.roles.values
|
||||
roles: (activityPlan?.roles.values ?? [])
|
||||
.sorted((a, b) => a.id.compareTo(b.id))
|
||||
.toList(),
|
||||
assignedRoles: activityRoles.values.toList(),
|
||||
|
|
|
|||
|
|
@ -128,12 +128,14 @@ class AnalyticsUpdateService {
|
|||
await future;
|
||||
}
|
||||
|
||||
Future<void> sendActivityAnalytics(String roomId) async {
|
||||
final analyticsRoom = await _getAnalyticsRoom();
|
||||
Future<void> sendActivityAnalytics(String roomId, LanguageModel lang) async {
|
||||
final analyticsRoom = await _getAnalyticsRoom(l2Override: lang);
|
||||
if (analyticsRoom == null) return;
|
||||
|
||||
await analyticsRoom.addActivityRoomId(roomId);
|
||||
dataService.updateDispatcher.sendActivityAnalyticsUpdate(roomId);
|
||||
if (lang.langCodeShort == _l2?.langCodeShort) {
|
||||
dataService.updateDispatcher.sendActivityAnalyticsUpdate(roomId);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> blockConstruct(ConstructIdentifier constructId) async {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart';
|
||||
|
|
@ -50,42 +49,110 @@ class ExampleMessageUtil {
|
|||
String? form,
|
||||
PangeaMessageEvent messageEvent,
|
||||
) {
|
||||
PangeaToken? token;
|
||||
String? text;
|
||||
List<PangeaToken>? tokens;
|
||||
int targetTokenIndex = -1;
|
||||
|
||||
if (messageEvent.isAudioMessage) {
|
||||
final stt = messageEvent.getSpeechToTextLocal();
|
||||
if (stt == null) return null;
|
||||
final tokens = stt.transcript.sttTokens.map((t) => t.token).toList();
|
||||
token = tokens.firstWhereOrNull(
|
||||
(token) => token.text.content == form,
|
||||
);
|
||||
|
||||
tokens = stt.transcript.sttTokens.map((t) => t.token).toList();
|
||||
targetTokenIndex = tokens.indexWhere((t) => t.text.content == form);
|
||||
text = stt.transcript.text;
|
||||
} else {
|
||||
final tokens = messageEvent.messageDisplayRepresentation?.tokens;
|
||||
tokens = messageEvent.messageDisplayRepresentation?.tokens;
|
||||
if (tokens == null || tokens.isEmpty) return null;
|
||||
token = tokens.firstWhereOrNull(
|
||||
(token) => token.text.content == form,
|
||||
);
|
||||
|
||||
targetTokenIndex = tokens.indexWhere((t) => t.text.content == form);
|
||||
text = messageEvent.messageDisplayText;
|
||||
}
|
||||
|
||||
if (token == null) return null;
|
||||
if (targetTokenIndex == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final before = text.characters.take(token.text.offset).toString();
|
||||
final after = text.characters
|
||||
.skip(token.text.offset + token.text.content.characters.length)
|
||||
final targetToken = tokens[targetTokenIndex];
|
||||
|
||||
const maxContextChars = 100;
|
||||
|
||||
final targetStart = targetToken.text.offset;
|
||||
final targetEnd = targetStart + targetToken.text.content.characters.length;
|
||||
|
||||
final totalChars = text.characters.length;
|
||||
|
||||
final beforeAvailable = targetStart;
|
||||
final afterAvailable = totalChars - targetEnd;
|
||||
|
||||
// ---------- Dynamic budget split ----------
|
||||
int beforeBudget = maxContextChars ~/ 2;
|
||||
int afterBudget = maxContextChars - beforeBudget;
|
||||
|
||||
if (beforeAvailable < beforeBudget) {
|
||||
afterBudget += beforeBudget - beforeAvailable;
|
||||
beforeBudget = beforeAvailable;
|
||||
} else if (afterAvailable < afterBudget) {
|
||||
beforeBudget += afterBudget - afterAvailable;
|
||||
afterBudget = afterAvailable;
|
||||
}
|
||||
|
||||
// ---------- BEFORE ----------
|
||||
int beforeStartOffset = 0;
|
||||
bool trimmedBefore = false;
|
||||
|
||||
if (beforeAvailable > beforeBudget) {
|
||||
final desiredStart = targetStart - beforeBudget;
|
||||
|
||||
for (int i = 0; i < targetTokenIndex; i++) {
|
||||
final token = tokens[i];
|
||||
final tokenEnd =
|
||||
token.text.offset + token.text.content.characters.length;
|
||||
|
||||
if (tokenEnd > desiredStart) {
|
||||
beforeStartOffset = token.text.offset;
|
||||
trimmedBefore = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final before = text.characters
|
||||
.skip(beforeStartOffset)
|
||||
.take(targetStart - beforeStartOffset)
|
||||
.toString();
|
||||
|
||||
// ---------- AFTER ----------
|
||||
int afterEndOffset = totalChars;
|
||||
bool trimmedAfter = false;
|
||||
|
||||
if (afterAvailable > afterBudget) {
|
||||
final desiredEnd = targetEnd + afterBudget;
|
||||
|
||||
for (int i = targetTokenIndex + 1; i < tokens.length; i++) {
|
||||
final token = tokens[i];
|
||||
if (token.text.offset >= desiredEnd) {
|
||||
afterEndOffset = token.text.offset;
|
||||
trimmedAfter = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final after = text.characters
|
||||
.skip(targetEnd)
|
||||
.take(afterEndOffset - targetEnd)
|
||||
.toString()
|
||||
.trimRight();
|
||||
|
||||
return [
|
||||
if (trimmedBefore) const TextSpan(text: '… '),
|
||||
TextSpan(text: before),
|
||||
TextSpan(
|
||||
text: token.text.content,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
text: targetToken.text.content,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
TextSpan(text: after),
|
||||
if (trimmedAfter) const TextSpan(text: '…'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
|
|
@ -25,6 +23,7 @@ import 'package:fluffychat/pangea/practice_activities/practice_activity_model.da
|
|||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AnalyticsPracticeView extends StatelessWidget {
|
||||
final AnalyticsPracticeState controller;
|
||||
|
|
@ -113,110 +112,78 @@ class _AnalyticsActivityView extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
TextStyle? titleStyle = isColumnMode
|
||||
? Theme.of(context).textTheme.titleLarge
|
||||
: Theme.of(context).textTheme.titleMedium;
|
||||
titleStyle = titleStyle?.copyWith(fontWeight: FontWeight.bold);
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
//per-activity instructions, add switch statement once there are more types
|
||||
const InstructionsInlineTooltip(
|
||||
instructionsEnum: InstructionsEnum.selectMeaning,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 24.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ValueListenableBuilder(
|
||||
valueListenable: controller.activityTarget,
|
||||
builder: (context, target, __) => target != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16.0,
|
||||
right: 16.0,
|
||||
top: 16.0,
|
||||
SizedBox(
|
||||
height: 75.0,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: controller.activityTarget,
|
||||
builder: (context, target, __) => target != null
|
||||
? Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
target.promptText(context),
|
||||
textAlign: TextAlign.center,
|
||||
style: titleStyle,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (controller.widget.type == ConstructTypeEnum.vocab)
|
||||
PhoneticTranscriptionWidget(
|
||||
text:
|
||||
target.target.tokens.first.vocabConstructID.lemma,
|
||||
textLanguage: MatrixState
|
||||
.pangeaController.userController.userL2!,
|
||||
style: const TextStyle(fontSize: 14.0),
|
||||
),
|
||||
child: Column(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Text(
|
||||
target.promptText(context),
|
||||
textAlign: TextAlign.center,
|
||||
style: FluffyThemes.isColumnMode(context)
|
||||
? Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
)
|
||||
: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (controller.widget.type ==
|
||||
ConstructTypeEnum.vocab)
|
||||
PhoneticTranscriptionWidget(
|
||||
text: target
|
||||
.target.tokens.first.vocabConstructID.lemma,
|
||||
textLanguage: MatrixState
|
||||
.pangeaController.userController.userL2!,
|
||||
style: const TextStyle(fontSize: 14.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
Flexible(
|
||||
fit: FlexFit.loose,
|
||||
child: SingleChildScrollView(
|
||||
child: _AnalyticsPracticeCenterContent(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _ActivityChoicesWidget(controller),
|
||||
),
|
||||
//reserve space for grammar category morph meaning to avoid shifting, but only in those questions
|
||||
AnimatedBuilder(
|
||||
animation: Listenable.merge([
|
||||
controller.activityState,
|
||||
controller.selectedMorphChoice,
|
||||
]),
|
||||
builder: (context, _) {
|
||||
final activityState = controller.activityState.value;
|
||||
final selectedChoice = controller.selectedMorphChoice.value;
|
||||
|
||||
final isGrammarCategory = activityState
|
||||
is AsyncLoaded<MultipleChoicePracticeActivityModel> &&
|
||||
activityState.value.activityType ==
|
||||
ActivityTypeEnum.grammarCategory;
|
||||
|
||||
if (!isGrammarCategory) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: 125.0,
|
||||
child: selectedChoice == null
|
||||
? const SizedBox.shrink()
|
||||
: SingleChildScrollView(
|
||||
child: MorphMeaningWidget(
|
||||
feature: selectedChoice.feature,
|
||||
tag: selectedChoice.tag,
|
||||
blankErrorFeedback: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Center(
|
||||
child: _AnalyticsPracticeCenterContent(controller: controller),
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
_ActivityChoicesWidget(controller),
|
||||
const SizedBox(height: 16.0),
|
||||
ListenableBuilder(
|
||||
listenable: Listenable.merge([
|
||||
controller.activityState,
|
||||
controller.selectedMorphChoice,
|
||||
]),
|
||||
builder: (context, _) {
|
||||
final activityState = controller.activityState.value;
|
||||
final selectedChoice = controller.selectedMorphChoice.value;
|
||||
|
||||
if (activityState
|
||||
is! AsyncLoaded<MultipleChoicePracticeActivityModel> ||
|
||||
selectedChoice == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return MorphMeaningWidget(
|
||||
feature: selectedChoice.feature,
|
||||
tag: selectedChoice.tag,
|
||||
blankErrorFeedback: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -235,29 +202,39 @@ class _AnalyticsPracticeCenterContent extends StatelessWidget {
|
|||
valueListenable: controller.activityTarget,
|
||||
builder: (context, target, __) => switch (target?.target.activityType) {
|
||||
null => const SizedBox(),
|
||||
ActivityTypeEnum.grammarError => ValueListenableBuilder(
|
||||
valueListenable: controller.activityState,
|
||||
builder: (context, state, __) => switch (state) {
|
||||
AsyncLoaded(
|
||||
value: final GrammarErrorPracticeActivityModel activity
|
||||
) =>
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_ErrorBlankWidget(
|
||||
key: ValueKey(
|
||||
'${activity.eventID}_${activity.errorOffset}_${activity.errorLength}',
|
||||
),
|
||||
activity: activity,
|
||||
ActivityTypeEnum.grammarError => SizedBox(
|
||||
height: 160.0,
|
||||
child: SingleChildScrollView(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: controller.activityState,
|
||||
builder: (context, state, __) => switch (state) {
|
||||
AsyncLoaded(
|
||||
value: final GrammarErrorPracticeActivityModel activity
|
||||
) =>
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_ErrorBlankWidget(
|
||||
key: ValueKey(
|
||||
'${activity.eventID}_${activity.errorOffset}_${activity.errorLength}',
|
||||
),
|
||||
activity: activity,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
_ => const SizedBox(),
|
||||
},
|
||||
_ => const SizedBox(),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
_ => _ExampleMessageWidget(
|
||||
controller.getExampleMessage(target!.target),
|
||||
_ => SizedBox(
|
||||
height: 100.0,
|
||||
child: Center(
|
||||
child: _ExampleMessageWidget(
|
||||
controller.getExampleMessage(target!.target),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
|
|
@ -333,6 +310,51 @@ class _ErrorBlankWidgetState extends State<_ErrorBlankWidget> {
|
|||
final errorOffset = widget.activity.errorOffset;
|
||||
final errorLength = widget.activity.errorLength;
|
||||
|
||||
const maxContextChars = 50;
|
||||
|
||||
final chars = text.characters;
|
||||
final totalLength = chars.length;
|
||||
|
||||
// ---------- BEFORE ----------
|
||||
int beforeStart = 0;
|
||||
bool trimmedBefore = false;
|
||||
|
||||
if (errorOffset > maxContextChars) {
|
||||
int desiredStart = errorOffset - maxContextChars;
|
||||
|
||||
// Snap left to nearest whitespace to avoid cutting words
|
||||
while (desiredStart > 0 && chars.elementAt(desiredStart) != ' ') {
|
||||
desiredStart--;
|
||||
}
|
||||
|
||||
beforeStart = desiredStart;
|
||||
trimmedBefore = true;
|
||||
}
|
||||
|
||||
final before =
|
||||
chars.skip(beforeStart).take(errorOffset - beforeStart).toString();
|
||||
|
||||
// ---------- AFTER ----------
|
||||
int afterEnd = totalLength;
|
||||
bool trimmedAfter = false;
|
||||
|
||||
final errorEnd = errorOffset + errorLength;
|
||||
final afterChars = totalLength - errorEnd;
|
||||
|
||||
if (afterChars > maxContextChars) {
|
||||
int desiredEnd = errorEnd + maxContextChars;
|
||||
|
||||
// Snap right to nearest whitespace
|
||||
while (desiredEnd < totalLength && chars.elementAt(desiredEnd) != ' ') {
|
||||
desiredEnd++;
|
||||
}
|
||||
|
||||
afterEnd = desiredEnd;
|
||||
trimmedAfter = true;
|
||||
}
|
||||
|
||||
final after = chars.skip(errorEnd).take(afterEnd - errorEnd).toString();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
|
|
@ -357,10 +379,8 @@ class _ErrorBlankWidgetState extends State<_ErrorBlankWidget> {
|
|||
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
),
|
||||
children: [
|
||||
if (errorOffset > 0)
|
||||
TextSpan(
|
||||
text: text.characters.take(errorOffset).toString(),
|
||||
),
|
||||
if (trimmedBefore) const TextSpan(text: '…'),
|
||||
if (before.isNotEmpty) TextSpan(text: before),
|
||||
WidgetSpan(
|
||||
child: Container(
|
||||
height: 4.0,
|
||||
|
|
@ -371,12 +391,8 @@ class _ErrorBlankWidgetState extends State<_ErrorBlankWidget> {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (errorOffset + errorLength < text.length)
|
||||
TextSpan(
|
||||
text: text.characters
|
||||
.skip(errorOffset + errorLength)
|
||||
.toString(),
|
||||
),
|
||||
if (after.isNotEmpty) TextSpan(text: after),
|
||||
if (trimmedAfter) const TextSpan(text: '…'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -464,43 +480,29 @@ class _ActivityChoicesWidget extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
AsyncLoaded<MultipleChoicePracticeActivityModel>(:final value) =>
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
ValueListenableBuilder(
|
||||
valueListenable: controller.enableChoicesNotifier,
|
||||
builder: (context, enabled, __) {
|
||||
final choices = controller.filteredChoices(value);
|
||||
final constrainedHeight =
|
||||
constraints.maxHeight.clamp(0.0, 400.0);
|
||||
final cardHeight = (constrainedHeight / (choices.length + 1))
|
||||
.clamp(50.0, 80.0);
|
||||
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: controller.enableChoicesNotifier,
|
||||
builder: (context, enabled, __) => Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: choices
|
||||
.map(
|
||||
(choice) => _ChoiceCard(
|
||||
activity: value,
|
||||
targetId: controller
|
||||
.choiceTargetId(choice.choiceId),
|
||||
choiceId: choice.choiceId,
|
||||
onPressed: () => controller.onSelectChoice(
|
||||
choice.choiceId,
|
||||
),
|
||||
cardHeight: cardHeight,
|
||||
choiceText: choice.choiceText,
|
||||
choiceEmoji: choice.choiceEmoji,
|
||||
enabled: enabled,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
return Column(
|
||||
spacing: 8.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: choices
|
||||
.map(
|
||||
(choice) => _ChoiceCard(
|
||||
activity: value,
|
||||
targetId: controller.choiceTargetId(choice.choiceId),
|
||||
choiceId: choice.choiceId,
|
||||
onPressed: () => controller.onSelectChoice(
|
||||
choice.choiceId,
|
||||
),
|
||||
cardHeight: 60.0,
|
||||
choiceText: choice.choiceText,
|
||||
choiceEmoji: choice.choiceEmoji,
|
||||
enabled: enabled,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -149,8 +149,6 @@ class _CardContainer extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: height,
|
||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/constants/bot_mode.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
|
||||
extension BotRoomExtension on Room {
|
||||
|
|
@ -23,11 +24,40 @@ extension BotRoomExtension on Room {
|
|||
return BotOptionsModel.fromJson(stateEvent.content);
|
||||
}
|
||||
|
||||
Future<void> setBotOptions(BotOptionsModel options) =>
|
||||
client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.botOptions,
|
||||
'',
|
||||
options.toJson(),
|
||||
);
|
||||
Future<void> setBotOptions(BotOptionsModel options) async {
|
||||
const maxRetries = 3;
|
||||
Duration retryDelay = const Duration(seconds: 5);
|
||||
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
if (attempt > 1) {
|
||||
await Future.delayed(retryDelay);
|
||||
retryDelay *= 2;
|
||||
}
|
||||
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.botOptions,
|
||||
'',
|
||||
options.toJson(),
|
||||
);
|
||||
|
||||
return;
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
'roomId': id,
|
||||
'options': options.toJson(),
|
||||
'attempt': attempt,
|
||||
},
|
||||
);
|
||||
|
||||
if (attempt == maxRetries) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ import 'package:fluffychat/l10n/l10n.dart';
|
|||
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/pages/pangea_invitation_selection_view.dart';
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/extensions/join_rule_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/user/user_search_extension.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -337,20 +337,11 @@ class PangeaInvitationSelectionController
|
|||
setState(() => foundProfiles = []);
|
||||
}
|
||||
|
||||
String pangeaSearchText = text;
|
||||
if (!pangeaSearchText.startsWith("@")) {
|
||||
pangeaSearchText = "@$pangeaSearchText";
|
||||
}
|
||||
if (!pangeaSearchText.contains(":")) {
|
||||
pangeaSearchText = "$pangeaSearchText:${Environment.homeServer}";
|
||||
}
|
||||
|
||||
setState(() => loading = true);
|
||||
final matrix = Matrix.of(context);
|
||||
SearchUserDirectoryResponse response;
|
||||
try {
|
||||
response =
|
||||
await matrix.client.searchUserDirectory(pangeaSearchText, limit: 100);
|
||||
response = await matrix.client.searchUser(text, limit: 100);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text((e).toLocalizedString(context))),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/constants/bot_mode.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/user/user_model.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -15,10 +17,15 @@ extension BotClientExtension on Client {
|
|||
Room? get botDM => rooms.firstWhereOrNull((r) => r.isBotDM);
|
||||
|
||||
// All 2-member rooms with the bot
|
||||
List<Room> get targetBotChats => rooms.where((r) {
|
||||
if (r.isBotDM) return true;
|
||||
if (r.summary.mJoinedMemberCount != 2) return false;
|
||||
return r.getParticipants().any((u) => u.id == BotName.byEnvironment);
|
||||
List<Room> get _targetBotChats => rooms.where((r) {
|
||||
return
|
||||
// bot settings exist
|
||||
r.botOptions != null &&
|
||||
// there is no activity plan
|
||||
r.activityPlan == null &&
|
||||
// it's just the bot and one other user in the room
|
||||
r.summary.mJoinedMemberCount == 2 &&
|
||||
r.getParticipants().any((u) => u.id == BotName.byEnvironment);
|
||||
}).toList();
|
||||
|
||||
Future<String> startChatWithBot() => startDirectChat(
|
||||
|
|
@ -42,30 +49,41 @@ extension BotClientExtension on Client {
|
|||
);
|
||||
|
||||
Future<void> updateBotOptions(UserSettings userSettings) async {
|
||||
final rooms = targetBotChats;
|
||||
if (rooms.isEmpty) return;
|
||||
final targetBotRooms = [..._targetBotChats];
|
||||
if (targetBotRooms.isEmpty) return;
|
||||
|
||||
final futures = <Future>[];
|
||||
for (final room in rooms) {
|
||||
final botOptions = room.botOptions ?? const BotOptionsModel();
|
||||
final targetLanguage = userSettings.targetLanguage;
|
||||
final languageLevel = userSettings.cefrLevel;
|
||||
final voice = userSettings.voice;
|
||||
try {
|
||||
final futures = <Future>[];
|
||||
for (final targetBotRoom in targetBotRooms) {
|
||||
final botOptions = targetBotRoom.botOptions ?? const BotOptionsModel();
|
||||
final targetLanguage = userSettings.targetLanguage;
|
||||
final languageLevel = userSettings.cefrLevel;
|
||||
final voice = userSettings.voice;
|
||||
|
||||
if (botOptions.targetLanguage == targetLanguage &&
|
||||
botOptions.languageLevel == languageLevel &&
|
||||
botOptions.targetVoice == voice) {
|
||||
continue;
|
||||
if (botOptions.targetLanguage == targetLanguage &&
|
||||
botOptions.languageLevel == languageLevel &&
|
||||
botOptions.targetVoice == voice) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final updated = botOptions.copyWith(
|
||||
targetLanguage: targetLanguage,
|
||||
languageLevel: languageLevel,
|
||||
targetVoice: voice,
|
||||
);
|
||||
futures.add(targetBotRoom.setBotOptions(updated));
|
||||
}
|
||||
|
||||
final updated = botOptions.copyWith(
|
||||
targetLanguage: targetLanguage,
|
||||
languageLevel: languageLevel,
|
||||
targetVoice: voice,
|
||||
await Future.wait(futures);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
'userSettings': userSettings.toJson(),
|
||||
'targetBotRooms': targetBotRooms.map((r) => r.id).toList(),
|
||||
},
|
||||
);
|
||||
futures.add(room.setBotOptions(updated));
|
||||
}
|
||||
|
||||
await Future.wait(futures);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:http/http.dart' hide Client;
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix/matrix_api_lite/generated/api.dart';
|
||||
|
|
@ -7,6 +8,8 @@ import 'package:matrix/matrix_api_lite/generated/api.dart';
|
|||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_roles_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_model.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/courses/course_plan_event.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
|
||||
extension RoomSummaryExtension on Api {
|
||||
|
|
@ -52,27 +55,40 @@ class RoomSummariesResponse {
|
|||
});
|
||||
return RoomSummariesResponse(summaries: summaries);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
summaries.forEach((key, value) {
|
||||
json[key] = value.toJson();
|
||||
});
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
class RoomSummaryResponse {
|
||||
final ActivityPlanModel activityPlan;
|
||||
final ActivityRolesModel activityRoles;
|
||||
final ActivityPlanModel? activityPlan;
|
||||
final ActivityRolesModel? activityRoles;
|
||||
final ActivitySummaryModel? activitySummary;
|
||||
final CoursePlanEvent? coursePlan;
|
||||
|
||||
final JoinRules? joinRule;
|
||||
final Map<String, int>? powerLevels;
|
||||
final Map<String, String> membershipSummary;
|
||||
final String? displayName;
|
||||
final String? avatarUrl;
|
||||
|
||||
RoomSummaryResponse({
|
||||
required this.activityPlan,
|
||||
required this.activityRoles,
|
||||
required this.membershipSummary,
|
||||
this.activityPlan,
|
||||
this.activityRoles,
|
||||
this.activitySummary,
|
||||
this.coursePlan,
|
||||
this.joinRule,
|
||||
this.powerLevels,
|
||||
this.displayName,
|
||||
this.avatarUrl,
|
||||
});
|
||||
|
||||
List<String> get adminUserIDs {
|
||||
if (powerLevels == null) return [];
|
||||
return powerLevels!.entries
|
||||
.where((entry) => entry.value >= 100)
|
||||
.map((entry) => entry.key)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Membership? getMembershipForUserId(String userId) {
|
||||
final membershipString = membershipSummary[userId];
|
||||
if (membershipString == null) return null;
|
||||
|
|
@ -83,32 +99,93 @@ class RoomSummaryResponse {
|
|||
}
|
||||
|
||||
Map<String, ActivityRoleModel> get joinedUsersWithRoles {
|
||||
if (activityRoles == null) return {};
|
||||
return Map.fromEntries(
|
||||
activityRoles.roles.entries.where(
|
||||
activityRoles!.roles.entries.where(
|
||||
(role) => getMembershipForUserId(role.value.userId) == Membership.join,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
factory RoomSummaryResponse.fromJson(Map<String, dynamic> json) {
|
||||
final planEntry =
|
||||
json[PangeaEventTypes.activityPlan]?["default"]?["content"];
|
||||
ActivityPlanModel? plan;
|
||||
if (planEntry != null && planEntry is Map<String, dynamic>) {
|
||||
plan = ActivityPlanModel.fromJson(planEntry);
|
||||
}
|
||||
|
||||
final rolesEntry =
|
||||
json[PangeaEventTypes.activityRole]?["default"]?["content"];
|
||||
ActivityRolesModel? roles;
|
||||
if (rolesEntry != null && rolesEntry is Map<String, dynamic>) {
|
||||
roles = ActivityRolesModel.fromJson(rolesEntry);
|
||||
}
|
||||
|
||||
final summaryEntry =
|
||||
json[PangeaEventTypes.activitySummary]?["default"]?["content"];
|
||||
ActivitySummaryModel? summary;
|
||||
if (summaryEntry != null && summaryEntry is Map<String, dynamic>) {
|
||||
summary = ActivitySummaryModel.fromJson(summaryEntry);
|
||||
}
|
||||
|
||||
final coursePlanEntry =
|
||||
json[PangeaEventTypes.coursePlan]?["default"]?["content"];
|
||||
CoursePlanEvent? coursePlan;
|
||||
if (coursePlanEntry != null && coursePlanEntry is Map<String, dynamic>) {
|
||||
coursePlan = CoursePlanEvent.fromJson(coursePlanEntry);
|
||||
}
|
||||
|
||||
final powerLevelsEntry =
|
||||
json[EventTypes.RoomPowerLevels]?['default']?['content']?['users'];
|
||||
Map<String, int>? powerLevels;
|
||||
if (powerLevelsEntry != null) {
|
||||
powerLevels = Map<String, int>.from(powerLevelsEntry);
|
||||
}
|
||||
|
||||
final joinRulesString =
|
||||
json[EventTypes.RoomJoinRules]?['default']?['content']?['join_rule'];
|
||||
JoinRules? joinRule;
|
||||
if (joinRulesString != null && joinRulesString is String) {
|
||||
joinRule = JoinRules.values
|
||||
.singleWhereOrNull((element) => element.text == joinRulesString);
|
||||
}
|
||||
|
||||
final displayName =
|
||||
json[EventTypes.RoomName]?['default']?['content']?['name'] as String?;
|
||||
|
||||
String? avatarUrl =
|
||||
json[EventTypes.RoomAvatar]?['default']?['content']?['url'] as String?;
|
||||
if (avatarUrl != null && Uri.tryParse(avatarUrl) == null) {
|
||||
avatarUrl = null;
|
||||
}
|
||||
|
||||
return RoomSummaryResponse(
|
||||
activityPlan: ActivityPlanModel.fromJson(
|
||||
json[PangeaEventTypes.activityPlan]?["default"]?["content"] ?? {},
|
||||
),
|
||||
activityRoles: ActivityRolesModel.fromJson(
|
||||
json[PangeaEventTypes.activityRole]?["default"]?["content"] ?? {},
|
||||
),
|
||||
activityPlan: plan,
|
||||
activityRoles: roles,
|
||||
activitySummary: summary,
|
||||
coursePlan: coursePlan,
|
||||
powerLevels: powerLevels,
|
||||
joinRule: joinRule,
|
||||
membershipSummary: Map<String, String>.from(
|
||||
json['membership_summary'] ?? {},
|
||||
),
|
||||
displayName: displayName,
|
||||
avatarUrl: avatarUrl,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
PangeaEventTypes.activityPlan: activityPlan.toJson(),
|
||||
PangeaEventTypes.activityRole: activityRoles.toJson(),
|
||||
'membership_summary': membershipSummary,
|
||||
'activityPlan': activityPlan?.toJson(),
|
||||
'activityRoles': activityRoles?.toJson(),
|
||||
'activitySummary': activitySummary?.toJson(),
|
||||
'coursePlan': coursePlan?.toJson(),
|
||||
'joinRule': joinRule?.text,
|
||||
'powerLevels': powerLevels,
|
||||
'membershipSummary': membershipSummary,
|
||||
'displayName': displayName,
|
||||
'avatarUrl': avatarUrl,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ class Choreographer extends ChangeNotifier {
|
|||
_choreoRecord = null;
|
||||
itController.closeIT();
|
||||
itController.clearSourceText();
|
||||
itController.clearDissmissed();
|
||||
itController.clearSession();
|
||||
igcController.clear();
|
||||
_resetDebounceTimer();
|
||||
_setChoreoMode(ChoreoModeEnum.igc);
|
||||
|
|
@ -372,7 +372,9 @@ class Choreographer extends ChangeNotifier {
|
|||
}
|
||||
|
||||
void _onCloseIT() {
|
||||
if (currentText.isEmpty && itController.sourceText.value != null) {
|
||||
if (itController.dismissed &&
|
||||
currentText.isEmpty &&
|
||||
itController.sourceText.value != null) {
|
||||
textController.setSystemText(
|
||||
itController.sourceText.value!,
|
||||
EditTypeEnum.itDismissed,
|
||||
|
|
|
|||
|
|
@ -65,8 +65,9 @@ class ITController {
|
|||
_sourceText.value = null;
|
||||
}
|
||||
|
||||
void clearDissmissed() {
|
||||
void clearSession() {
|
||||
dismissed = false;
|
||||
_progress.value = 0.0;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
|
|
@ -105,6 +106,7 @@ class ITController {
|
|||
_queue.clear();
|
||||
_currentITStep.value = null;
|
||||
_goldRouteTracker = null;
|
||||
_progress.value = 0.0;
|
||||
_sourceText.value = text;
|
||||
setEditingSourceText(false);
|
||||
_continueIT();
|
||||
|
|
@ -152,7 +154,6 @@ class ITController {
|
|||
) +
|
||||
1) /
|
||||
_goldRouteTracker!.continuances.length;
|
||||
debugPrint("Progress updated to $progress");
|
||||
_progress.value = progress;
|
||||
_continueIT();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,30 +22,33 @@ class ShimmerBackground extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!enabled) {
|
||||
return child;
|
||||
}
|
||||
|
||||
final borderRadius =
|
||||
this.borderRadius ?? BorderRadius.circular(AppConfig.borderRadius);
|
||||
return Stack(
|
||||
children: [
|
||||
child,
|
||||
if (enabled)
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: ClipRRect(
|
||||
borderRadius: borderRadius,
|
||||
child: Shimmer.fromColors(
|
||||
baseColor: baseColor ?? shimmerColor.withValues(alpha: 0.1),
|
||||
highlightColor: shimmerColor.withValues(alpha: 0.6),
|
||||
direction: ShimmerDirection.ltr,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: shimmerColor.withValues(alpha: 0.3),
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: ClipRRect(
|
||||
borderRadius: borderRadius,
|
||||
child: Shimmer.fromColors(
|
||||
baseColor: baseColor ?? shimmerColor.withValues(alpha: 0.1),
|
||||
highlightColor: shimmerColor.withValues(alpha: 0.6),
|
||||
direction: ShimmerDirection.ltr,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: shimmerColor.withValues(alpha: 0.3),
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ class ShrinkableText extends StatelessWidget {
|
|||
final String text;
|
||||
final double maxWidth;
|
||||
final TextStyle? style;
|
||||
final Alignment? alignment;
|
||||
|
||||
const ShrinkableText({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.maxWidth,
|
||||
this.alignment,
|
||||
this.style,
|
||||
});
|
||||
|
||||
|
|
@ -18,6 +20,7 @@ class ShrinkableText extends StatelessWidget {
|
|||
builder: (context, constraints) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
alignment: alignment,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
|
|
|
|||
|
|
@ -133,9 +133,13 @@ class CourseChatsController extends State<CourseChats>
|
|||
}
|
||||
|
||||
final activity = summary.activityPlan;
|
||||
final roles = summary.activityRoles;
|
||||
final users = summary.joinedUsersWithRoles;
|
||||
|
||||
if (users.isEmpty || !validIDs.contains(activity.activityId)) {
|
||||
if (activity == null ||
|
||||
roles == null ||
|
||||
users.isEmpty ||
|
||||
!validIDs.contains(activity.activityId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -148,7 +152,7 @@ class CourseChatsController extends State<CourseChats>
|
|||
// It's possible for users to finish an activity and then for some of the
|
||||
// users to leave, but if the activity was archived by anyone, that means
|
||||
// it was full at some point.
|
||||
if (summary.activityRoles.roles.values.any((role) => role.isArchived)) {
|
||||
if (roles.roles.values.any((role) => role.isArchived)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -233,7 +237,10 @@ class CourseChatsController extends State<CourseChats>
|
|||
Logs().w('Unable to load hierarchy', e, s);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(e.toLocalizedString(context))),
|
||||
SnackBar(
|
||||
content: Text(e.toLocalizedString(context)),
|
||||
showCloseIcon: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
|
|
|
|||
188
lib/pangea/course_creation/public_course_preview.dart
Normal file
188
lib/pangea/course_creation/public_course_preview.dart
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/room_summary_extension.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/course_creation/public_course_preview_view.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/course_activities/activity_summaries_provider.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/courses/course_plan_builder.dart';
|
||||
import 'package:fluffychat/pangea/join_codes/space_code_controller.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class PublicCoursePreview extends StatefulWidget {
|
||||
final String? roomID;
|
||||
|
||||
const PublicCoursePreview({
|
||||
super.key,
|
||||
required this.roomID,
|
||||
});
|
||||
|
||||
@override
|
||||
PublicCoursePreviewController createState() =>
|
||||
PublicCoursePreviewController();
|
||||
}
|
||||
|
||||
class PublicCoursePreviewController extends State<PublicCoursePreview>
|
||||
with CoursePlanProvider, ActivitySummariesProvider {
|
||||
RoomSummaryResponse? roomSummary;
|
||||
Object? roomSummaryError;
|
||||
bool loadingRoomSummary = false;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
_loadSummary();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant PublicCoursePreview oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.roomID != oldWidget.roomID) {
|
||||
_loadSummary();
|
||||
}
|
||||
}
|
||||
|
||||
bool get loading => loadingCourse || loadingRoomSummary;
|
||||
bool get hasError =>
|
||||
(courseError != null || (!loadingCourse && course == null)) ||
|
||||
(roomSummaryError != null ||
|
||||
(!loadingRoomSummary && roomSummary == null));
|
||||
|
||||
Future<void> _loadSummary() async {
|
||||
try {
|
||||
if (widget.roomID == null) {
|
||||
throw Exception("roomID is required");
|
||||
}
|
||||
|
||||
setState(() {
|
||||
loadingRoomSummary = true;
|
||||
roomSummaryError = null;
|
||||
});
|
||||
|
||||
await loadRoomSummaries([widget.roomID!]);
|
||||
if (roomSummaries == null || !roomSummaries!.containsKey(widget.roomID)) {
|
||||
throw Exception("Room summary not found");
|
||||
}
|
||||
|
||||
roomSummary = roomSummaries![widget.roomID];
|
||||
} catch (e, s) {
|
||||
roomSummaryError = e;
|
||||
loadingCourse = false;
|
||||
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {'roomID': widget.roomID, 'roomSummary': roomSummary?.toJson()},
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
loadingRoomSummary = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (roomSummary?.coursePlan != null) {
|
||||
await loadCourse(roomSummary!.coursePlan!.uuid).then((_) => loadTopics());
|
||||
} else {
|
||||
ErrorHandler.logError(
|
||||
e: Exception("No course plan found in room summary"),
|
||||
data: {'roomID': widget.roomID, 'roomSummary': roomSummary?.toJson()},
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
roomSummaryError = Exception("No course plan found in room summary");
|
||||
loadingCourse = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> joinWithCode(String code) async {
|
||||
if (code.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final roomId = await SpaceCodeController.joinSpaceWithCode(
|
||||
context,
|
||||
code,
|
||||
);
|
||||
|
||||
if (roomId != null) {
|
||||
final room = Matrix.of(context).client.getRoomById(roomId);
|
||||
room?.isSpace ?? true
|
||||
? context.go('/rooms/spaces/$roomId/details')
|
||||
: context.go('/rooms/$roomId');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> joinCourse() async {
|
||||
if (widget.roomID == null) {
|
||||
throw Exception("roomID is required");
|
||||
}
|
||||
|
||||
final roomID = widget.roomID;
|
||||
|
||||
final client = Matrix.of(context).client;
|
||||
final r = client.getRoomById(roomID!);
|
||||
if (r != null && r.membership == Membership.join) {
|
||||
if (mounted) {
|
||||
context.go("/rooms/spaces/${r.id}/details");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final knock = roomSummary?.joinRule == JoinRules.knock;
|
||||
final resp = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
String roomId;
|
||||
try {
|
||||
roomId = knock
|
||||
? await client.knockRoom(widget.roomID!)
|
||||
: await client.joinRoom(widget.roomID!);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {'roomID': widget.roomID},
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
|
||||
Room? room = client.getRoomById(roomId);
|
||||
if (!knock && room?.membership != Membership.join) {
|
||||
await client.waitForRoomInSync(roomId, join: true);
|
||||
room = client.getRoomById(roomId);
|
||||
}
|
||||
|
||||
if (knock) return;
|
||||
if (room == null) {
|
||||
ErrorHandler.logError(
|
||||
e: Exception("Failed to load joined room in public course preview"),
|
||||
data: {'roomID': widget.roomID},
|
||||
);
|
||||
throw Exception("Failed to join room");
|
||||
}
|
||||
context.go("/rooms/spaces/$roomId/details");
|
||||
},
|
||||
);
|
||||
|
||||
if (!knock || resp.isError) return;
|
||||
await showOkAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).youHaveKnocked,
|
||||
message: L10n.of(context).knockDesc,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => PublicCoursePreviewView(this);
|
||||
}
|
||||
388
lib/pangea/course_creation/public_course_preview_view.dart
Normal file
388
lib/pangea/course_creation/public_course_preview_view.dart
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/room_summary_extension.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart';
|
||||
import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart';
|
||||
import 'package:fluffychat/pangea/course_creation/public_course_preview.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/map_clipper.dart';
|
||||
import 'package:fluffychat/pangea/course_settings/pin_clipper.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/user_dialog.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class PublicCoursePreviewView extends StatelessWidget {
|
||||
final PublicCoursePreviewController controller;
|
||||
const PublicCoursePreviewView(
|
||||
this.controller, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
const double titleFontSize = 16.0;
|
||||
const double descFontSize = 12.0;
|
||||
|
||||
const double largeIconSize = 24.0;
|
||||
const double smallIconSize = 12.0;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10n.of(context).joinWithClassCode),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Container(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 500.0),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (controller.loading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.hasError) {
|
||||
return Center(
|
||||
child: ErrorIndicator(
|
||||
message: L10n.of(context).oopsSomethingWentWrong,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final course = controller.course!;
|
||||
final summary = controller.roomSummary!;
|
||||
|
||||
Uri? avatarUrl = course.imageUrl;
|
||||
if (summary.avatarUrl != null) {
|
||||
avatarUrl = Uri.tryParse(summary.avatarUrl!);
|
||||
}
|
||||
|
||||
final displayname = summary.displayName ?? course.title;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 12.0,
|
||||
left: 12.0,
|
||||
right: 12.0,
|
||||
),
|
||||
child: ListView.builder(
|
||||
itemCount: course.topicIds.length + 2,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return Column(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
ClipPath(
|
||||
clipper: MapClipper(),
|
||||
child: ImageByUrl(
|
||||
imageUrl: avatarUrl,
|
||||
width: 100.0,
|
||||
borderRadius: BorderRadius.circular(0.0),
|
||||
replacement: Avatar(
|
||||
name: displayname,
|
||||
size: 100.0,
|
||||
borderRadius: BorderRadius.circular(
|
||||
0.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
displayname,
|
||||
style: const TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
),
|
||||
),
|
||||
if (summary.adminUserIDs.isNotEmpty)
|
||||
_CourseAdminDisplay(summary),
|
||||
Text(
|
||||
course.description,
|
||||
style: const TextStyle(
|
||||
fontSize: descFontSize,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
spacing: 8.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CourseInfoChips(
|
||||
course.uuid,
|
||||
fontSize: descFontSize,
|
||||
iconSize: smallIconSize,
|
||||
),
|
||||
CourseInfoChip(
|
||||
icon: Icons.person,
|
||||
text:
|
||||
L10n.of(context).countParticipants(
|
||||
summary.membershipSummary.length,
|
||||
),
|
||||
fontSize: descFontSize,
|
||||
iconSize: smallIconSize,
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4.0,
|
||||
bottom: 8.0,
|
||||
),
|
||||
child: Row(
|
||||
spacing: 4.0,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.map,
|
||||
size: largeIconSize,
|
||||
),
|
||||
Text(
|
||||
L10n.of(context).coursePlan,
|
||||
style: const TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
index--;
|
||||
|
||||
if (index >= course.topicIds.length) {
|
||||
return const SizedBox(height: 12.0);
|
||||
}
|
||||
|
||||
final topicId = course.topicIds[index];
|
||||
final topic = course.loadedTopics[topicId];
|
||||
|
||||
if (topic == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipPath(
|
||||
clipper: PinClipper(),
|
||||
child: ImageByUrl(
|
||||
imageUrl: topic.imageUrl,
|
||||
width: 45.0,
|
||||
replacement: Container(
|
||||
width: 45.0,
|
||||
height: 45.0,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
topic.title,
|
||||
style: const TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
topic.description,
|
||||
style: const TextStyle(
|
||||
fontSize: descFontSize,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsetsGeometry
|
||||
.symmetric(
|
||||
vertical: 2.0,
|
||||
),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (topic.location != null)
|
||||
CourseInfoChip(
|
||||
icon: Icons.location_on,
|
||||
text: topic.location!,
|
||||
fontSize: descFontSize,
|
||||
iconSize: smallIconSize,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: theme.dividerColor,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Column(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
if (summary.joinRule == JoinRules.knock) ...[
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.of(context).enterCodeToJoin,
|
||||
),
|
||||
onSubmitted: controller.joinWithCode,
|
||||
),
|
||||
Row(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
const Expanded(
|
||||
child: Divider(),
|
||||
),
|
||||
Text(L10n.of(context).or),
|
||||
const Expanded(
|
||||
child: Divider(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
theme.colorScheme.primaryContainer,
|
||||
foregroundColor:
|
||||
theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
onPressed: controller.joinCourse,
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.map_outlined),
|
||||
Text(
|
||||
summary.joinRule == JoinRules.knock
|
||||
? L10n.of(context).knock
|
||||
: L10n.of(context).join,
|
||||
style: const TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CourseAdminDisplay extends StatelessWidget {
|
||||
final RoomSummaryResponse summary;
|
||||
const _CourseAdminDisplay(this.summary);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 12.0,
|
||||
runSpacing: 12.0,
|
||||
children: [
|
||||
...summary.adminUserIDs.map((adminId) {
|
||||
return FutureBuilder(
|
||||
future: Matrix.of(context).client.getProfileFromUserId(
|
||||
adminId,
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
final profile = snapshot.data;
|
||||
final displayName =
|
||||
profile?.displayName ?? adminId.localpart ?? adminId;
|
||||
return InkWell(
|
||||
onTap: profile != null
|
||||
? () => UserDialog.show(
|
||||
context: context,
|
||||
profile: profile,
|
||||
)
|
||||
: null,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(18.0),
|
||||
),
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Opacity(
|
||||
opacity: 0.5,
|
||||
child: Row(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Avatar(
|
||||
size: 18.0,
|
||||
mxContent: profile?.avatarUrl,
|
||||
name: displayName,
|
||||
userId: adminId,
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 80.0,
|
||||
),
|
||||
child: Text(
|
||||
displayName,
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart' as sdk;
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/course_creation/selected_course_view.dart';
|
||||
|
|
@ -12,11 +11,11 @@ import 'package:fluffychat/pangea/course_plans/courses/course_plan_builder.dart'
|
|||
import 'package:fluffychat/pangea/course_plans/courses/course_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/courses/course_plan_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/join_codes/space_code_controller.dart';
|
||||
import 'package:fluffychat/pangea/spaces/client_spaces_extension.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
enum SelectedCourseMode { launch, addToSpace, join }
|
||||
enum SelectedCourseMode { launch, addToSpace }
|
||||
|
||||
class SelectedCourse extends StatefulWidget {
|
||||
final String courseId;
|
||||
|
|
@ -26,15 +25,11 @@ class SelectedCourse extends StatefulWidget {
|
|||
/// In join mode, the ID of the space to join that already has this course.
|
||||
final String? spaceId;
|
||||
|
||||
/// In join mode, the room info for the space that already has this course.
|
||||
final PublicRoomsChunk? roomChunk;
|
||||
|
||||
const SelectedCourse(
|
||||
this.courseId,
|
||||
this.mode, {
|
||||
super.key,
|
||||
this.spaceId,
|
||||
this.roomChunk,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -63,8 +58,6 @@ class SelectedCourseController extends State<SelectedCourse>
|
|||
return L10n.of(context).newCourse;
|
||||
case SelectedCourseMode.addToSpace:
|
||||
return L10n.of(context).addCoursePlan;
|
||||
case SelectedCourseMode.join:
|
||||
return L10n.of(context).joinWithClassCode;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,10 +67,24 @@ class SelectedCourseController extends State<SelectedCourse>
|
|||
return L10n.of(context).createCourse;
|
||||
case SelectedCourseMode.addToSpace:
|
||||
return L10n.of(context).addCoursePlan;
|
||||
case SelectedCourseMode.join:
|
||||
return widget.roomChunk?.joinRule == JoinRules.knock.name
|
||||
? L10n.of(context).knock
|
||||
: L10n.of(context).join;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> joinWithCode(String code) async {
|
||||
if (code.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final roomId = await SpaceCodeController.joinSpaceWithCode(
|
||||
context,
|
||||
code,
|
||||
);
|
||||
|
||||
if (roomId != null) {
|
||||
final room = Matrix.of(context).client.getRoomById(roomId);
|
||||
room?.isSpace ?? true
|
||||
? context.go('/rooms/spaces/$roomId/details')
|
||||
: context.go('/rooms/$roomId');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -87,8 +94,6 @@ class SelectedCourseController extends State<SelectedCourse>
|
|||
return launchCourse(widget.courseId, course);
|
||||
case SelectedCourseMode.addToSpace:
|
||||
return addCourseToSpace(course);
|
||||
case SelectedCourseMode.join:
|
||||
return joinCourse();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -149,50 +154,6 @@ class SelectedCourseController extends State<SelectedCourse>
|
|||
context.go("/rooms/spaces/${space.id}/details?tab=course");
|
||||
}
|
||||
|
||||
Future<void> joinCourse() async {
|
||||
if (widget.roomChunk == null) {
|
||||
throw Exception("Room chunk is null");
|
||||
}
|
||||
|
||||
final client = Matrix.of(context).client;
|
||||
final r = client.getRoomById(widget.roomChunk!.roomId);
|
||||
if (r != null && r.membership == Membership.join) {
|
||||
if (mounted) {
|
||||
context.go("/rooms/spaces/${r.id}/details");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final knock = widget.roomChunk!.joinRule == JoinRules.knock.name;
|
||||
final roomId = widget.roomChunk != null && knock
|
||||
? await client.knockRoom(widget.roomChunk!.roomId)
|
||||
: await client.joinRoom(widget.roomChunk!.roomId);
|
||||
|
||||
Room? room = client.getRoomById(roomId);
|
||||
if (!knock && room?.membership != Membership.join) {
|
||||
await client.waitForRoomInSync(roomId, join: true);
|
||||
room = client.getRoomById(roomId);
|
||||
}
|
||||
|
||||
if (knock) {
|
||||
Navigator.of(context).pop();
|
||||
await showOkAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).youHaveKnocked,
|
||||
message: L10n.of(context).knockDesc,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (room == null) {
|
||||
throw Exception("Failed to join room");
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
context.go("/rooms/spaces/$roomId/details");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SelectedCourseView(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,13 +60,7 @@ class SelectedCourseView extends StatelessWidget {
|
|||
child: ListView.builder(
|
||||
itemCount: course.topicIds.length + 2,
|
||||
itemBuilder: (context, index) {
|
||||
String displayname = course.title;
|
||||
final roomChunk = controller.widget.roomChunk;
|
||||
if (roomChunk != null) {
|
||||
displayname = roomChunk.name ??
|
||||
roomChunk.canonicalAlias ??
|
||||
L10n.of(context).emptyChat;
|
||||
}
|
||||
final String displayname = course.title;
|
||||
|
||||
if (index == 0) {
|
||||
return Column(
|
||||
|
|
@ -75,9 +69,7 @@ class SelectedCourseView extends StatelessWidget {
|
|||
ClipPath(
|
||||
clipper: MapClipper(),
|
||||
child: ImageByUrl(
|
||||
imageUrl: controller.widget
|
||||
.roomChunk?.avatarUrl ??
|
||||
course.imageUrl,
|
||||
imageUrl: course.imageUrl,
|
||||
width: 100.0,
|
||||
borderRadius:
|
||||
BorderRadius.circular(0.0),
|
||||
|
|
@ -233,70 +225,74 @@ class SelectedCourseView extends StatelessWidget {
|
|||
spacing: 8.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (controller.widget.mode !=
|
||||
SelectedCourseMode.join) ...[
|
||||
Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.edit,
|
||||
size: mediumIconSize,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
L10n.of(context).editCourseLater,
|
||||
style: const TextStyle(
|
||||
fontSize: descFontSize,
|
||||
),
|
||||
Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.edit,
|
||||
size: mediumIconSize,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
L10n.of(context).editCourseLater,
|
||||
style: const TextStyle(
|
||||
fontSize: descFontSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.shield,
|
||||
size: mediumIconSize,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
L10n.of(context).newCourseAccess,
|
||||
style: const TextStyle(
|
||||
fontSize: descFontSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.shield,
|
||||
size: mediumIconSize,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
L10n.of(context).newCourseAccess,
|
||||
style: const TextStyle(
|
||||
fontSize: descFontSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
theme.colorScheme.primaryContainer,
|
||||
foregroundColor:
|
||||
theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
onPressed: () => showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => controller.submit(course),
|
||||
),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.map_outlined),
|
||||
Text(
|
||||
controller.buttonText,
|
||||
style: const TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
),
|
||||
child: Column(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme
|
||||
.colorScheme.primaryContainer,
|
||||
foregroundColor: theme
|
||||
.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () =>
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
controller.submit(course),
|
||||
),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.map_outlined),
|
||||
Text(
|
||||
controller.buttonText,
|
||||
style: const TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ mixin ActivitySummariesProvider<T extends StatefulWidget> on State<T> {
|
|||
|
||||
final activityPlan = roomSummary.activityPlan;
|
||||
final assignedRoles = roomSummary.joinedUsersWithRoles;
|
||||
if (activityPlan == null) return false;
|
||||
return activityPlan.roles.length - assignedRoles.length <= 0;
|
||||
}
|
||||
|
||||
|
|
@ -56,6 +57,7 @@ mixin ActivitySummariesProvider<T extends StatefulWidget> on State<T> {
|
|||
if (roomSummary == null) return false;
|
||||
|
||||
final activityRoles = roomSummary.activityRoles;
|
||||
if (activityRoles == null) return false;
|
||||
final roles = activityRoles.roles.values.where(
|
||||
(r) => r.userId != BotName.byEnvironment,
|
||||
);
|
||||
|
|
@ -76,7 +78,7 @@ mixin ActivitySummariesProvider<T extends StatefulWidget> on State<T> {
|
|||
Map<String, RoomSummaryResponse> activitySessions(String activityId) =>
|
||||
Map.fromEntries(
|
||||
roomSummaries?.entries
|
||||
.where((v) => v.value.activityPlan.activityId == activityId) ??
|
||||
.where((v) => v.value.activityPlan?.activityId == activityId) ??
|
||||
[],
|
||||
);
|
||||
|
||||
|
|
@ -115,7 +117,7 @@ mixin ActivitySummariesProvider<T extends StatefulWidget> on State<T> {
|
|||
final summary = entry.value;
|
||||
final roomId = entry.key;
|
||||
|
||||
if (summary.activityPlan.activityId != activityId) {
|
||||
if (summary.activityPlan?.activityId != activityId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -132,11 +134,13 @@ mixin ActivitySummariesProvider<T extends StatefulWidget> on State<T> {
|
|||
if (roomSummaries == null || roomSummaries!.isEmpty) return {};
|
||||
return roomSummaries!.values
|
||||
.where(
|
||||
(entry) => entry.activityRoles.roles.values.any(
|
||||
(v) => v.userId == userID && v.isArchived,
|
||||
),
|
||||
(entry) =>
|
||||
entry.activityRoles != null &&
|
||||
entry.activityRoles!.roles.values.any(
|
||||
(v) => v.userId == userID && v.isArchived,
|
||||
),
|
||||
)
|
||||
.map((e) => e.activityPlan.activityId)
|
||||
.map((e) => e.activityPlan?.activityId)
|
||||
.whereType<String>()
|
||||
.toSet();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/courses/course_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/courses/get_localized_courses_request.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -47,7 +48,12 @@ mixin CoursePlanProvider<T extends StatefulWidget> on State<T> {
|
|||
),
|
||||
);
|
||||
await course!.fetchMediaUrls();
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {'courseId': courseId},
|
||||
);
|
||||
courseError = e;
|
||||
} finally {
|
||||
if (mounted) setState(() => loadingCourse = false);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
|
|
@ -54,6 +55,17 @@ class CourseSettings extends StatelessWidget {
|
|||
}
|
||||
|
||||
if (controller.course == null || controller.courseError != null) {
|
||||
if (controller.courseError is Response &&
|
||||
(controller.courseError as Response).statusCode == 500) {
|
||||
return Center(
|
||||
child: Text(
|
||||
L10n.of(context).courseLoadingError,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return room.canChangeStateEvent(PangeaEventTypes.coursePlan)
|
||||
? Column(
|
||||
spacing: 50.0,
|
||||
|
|
|
|||
|
|
@ -32,9 +32,9 @@ enum InstructionsEnum {
|
|||
setLemmaEmoji,
|
||||
disableLanguageTools,
|
||||
selectMeaning,
|
||||
clickTextMessages,
|
||||
clickAudioMessages,
|
||||
dismissSupportChat,
|
||||
shimmerNewToken,
|
||||
shimmerTranslation,
|
||||
}
|
||||
|
||||
extension InstructionsEnumExtension on InstructionsEnum {
|
||||
|
|
@ -66,9 +66,9 @@ extension InstructionsEnumExtension on InstructionsEnum {
|
|||
case InstructionsEnum.noSavedActivitiesYet:
|
||||
case InstructionsEnum.setLemmaEmoji:
|
||||
case InstructionsEnum.disableLanguageTools:
|
||||
case InstructionsEnum.clickTextMessages:
|
||||
case InstructionsEnum.clickAudioMessages:
|
||||
case InstructionsEnum.dismissSupportChat:
|
||||
case InstructionsEnum.shimmerNewToken:
|
||||
case InstructionsEnum.shimmerTranslation:
|
||||
ErrorHandler.logError(
|
||||
e: Exception("No title for this instruction"),
|
||||
m: 'InstructionsEnumExtension.title',
|
||||
|
|
@ -130,9 +130,9 @@ extension InstructionsEnumExtension on InstructionsEnum {
|
|||
case InstructionsEnum.noSavedActivitiesYet:
|
||||
return l10n.noSavedActivitiesYet;
|
||||
case InstructionsEnum.setLemmaEmoji:
|
||||
case InstructionsEnum.clickTextMessages:
|
||||
case InstructionsEnum.clickAudioMessages:
|
||||
case InstructionsEnum.dismissSupportChat:
|
||||
case InstructionsEnum.shimmerNewToken:
|
||||
case InstructionsEnum.shimmerTranslation:
|
||||
return "";
|
||||
case InstructionsEnum.disableLanguageTools:
|
||||
return l10n.disableLanguageToolsDesc;
|
||||
|
|
|
|||
|
|
@ -391,6 +391,14 @@ class _PublicCourseTile extends StatelessWidget {
|
|||
this.course,
|
||||
});
|
||||
|
||||
void _navigateToCoursePage(
|
||||
BuildContext context,
|
||||
) {
|
||||
context.go(
|
||||
'/rooms/course/${Uri.encodeComponent(chunk.room.roomId)}',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
|
@ -411,10 +419,7 @@ class _PublicCourseTile extends StatelessWidget {
|
|||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
onTap: () => context.go(
|
||||
'/rooms/course/$courseId',
|
||||
extra: space,
|
||||
),
|
||||
onTap: () => _navigateToCoursePage(context),
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
|
|
@ -490,10 +495,7 @@ class _PublicCourseTile extends StatelessWidget {
|
|||
const SizedBox(height: 12.0),
|
||||
HoverBuilder(
|
||||
builder: (context, hovered) => ElevatedButton(
|
||||
onPressed: () => context.go(
|
||||
'/rooms/course/$courseId',
|
||||
extra: space,
|
||||
),
|
||||
onPressed: () => _navigateToCoursePage(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
theme.colorScheme.primaryContainer.withAlpha(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import 'package:go_router/go_router.dart';
|
|||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/shimmer_background.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/shrinkable_text.dart';
|
||||
import 'package:fluffychat/pangea/languages/language_model.dart';
|
||||
import 'package:fluffychat/pangea/languages/language_service.dart';
|
||||
import 'package:fluffychat/pangea/languages/p_language_store.dart';
|
||||
|
|
@ -102,14 +104,41 @@ class LanguageSelectionPageState extends State<LanguageSelectionPage> {
|
|||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10n.of(context).languages),
|
||||
title: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 500,
|
||||
),
|
||||
child: Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
BackButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return ShrinkableText(
|
||||
text: L10n.of(context).onboardingLanguagesTitle,
|
||||
maxWidth: constraints.maxWidth,
|
||||
alignment: Alignment.center,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 40.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 450,
|
||||
maxWidth: 500,
|
||||
),
|
||||
child: Column(
|
||||
spacing: 24.0,
|
||||
|
|
@ -121,6 +150,7 @@ class LanguageSelectionPageState extends State<LanguageSelectionPage> {
|
|||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
hintText: L10n.of(context).searchLanguagesHint,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
|
|
@ -153,27 +183,38 @@ class LanguageSelectionPageState extends State<LanguageSelectionPage> {
|
|||
),
|
||||
)
|
||||
.map(
|
||||
(l) => FilterChip(
|
||||
selected: _selectedLanguage == l,
|
||||
backgroundColor:
|
||||
_selectedLanguage == l
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.surface,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 4.0,
|
||||
(l) => ShimmerBackground(
|
||||
enabled: _selectedLanguage == null,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(16.0),
|
||||
),
|
||||
label: Text(
|
||||
l.getDisplayName(context),
|
||||
style: isColumnMode
|
||||
? theme.textTheme.bodyLarge
|
||||
: theme.textTheme.bodyMedium,
|
||||
child: FilterChip(
|
||||
selected: _selectedLanguage == l,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(16.0),
|
||||
),
|
||||
),
|
||||
backgroundColor:
|
||||
_selectedLanguage == l
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.surface,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
label: Text(
|
||||
l.getDisplayName(context),
|
||||
style: isColumnMode
|
||||
? theme.textTheme.bodyLarge
|
||||
: theme.textTheme.bodyMedium,
|
||||
),
|
||||
onSelected: (selected) {
|
||||
_setSelectedLanguage(
|
||||
selected ? l : null,
|
||||
);
|
||||
},
|
||||
),
|
||||
onSelected: (selected) {
|
||||
_setSelectedLanguage(
|
||||
selected ? l : null,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
|
|
@ -220,23 +261,24 @@ class LanguageSelectionPageState extends State<LanguageSelectionPage> {
|
|||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
Text(
|
||||
L10n.of(context).chooseLanguage,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _selectedLanguage != null ? _submit : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primaryContainer,
|
||||
foregroundColor: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(L10n.of(context).letsGo),
|
||||
],
|
||||
ShimmerBackground(
|
||||
enabled: _selectedLanguage != null,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: _selectedLanguage != null ? _submit : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primaryContainer,
|
||||
foregroundColor: theme.colorScheme.onPrimaryContainer,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(L10n.of(context).letsGo),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -21,9 +21,24 @@ class LoginOptionsView extends StatelessWidget {
|
|||
final theme = Theme.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
L10n.of(context).loginToAccount,
|
||||
title: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 450,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
BackButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Text(L10n.of(context).login),
|
||||
const SizedBox(
|
||||
width: 40.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
|
|
@ -36,6 +51,13 @@ class LoginOptionsView extends StatelessWidget {
|
|||
spacing: 16.0,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).loginToAccount,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const PangeaSsoButton(
|
||||
provider: SSOProvider.apple,
|
||||
title: "Apple",
|
||||
|
|
|
|||
|
|
@ -15,9 +15,24 @@ class PasswordLoginView extends StatelessWidget {
|
|||
key: controller.formKey,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
L10n.of(context).loginWithEmail,
|
||||
title: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 450,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
BackButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Text(L10n.of(context).loginWithEmail),
|
||||
const SizedBox(
|
||||
width: 40.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
|
|
|
|||
|
|
@ -27,7 +27,24 @@ class SignupPageView extends StatelessWidget {
|
|||
return Form(
|
||||
key: controller.formKey,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(),
|
||||
appBar: AppBar(
|
||||
title: SizedBox(
|
||||
width: 450,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
BackButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Text(L10n.of(context).signUp),
|
||||
const SizedBox(
|
||||
width: 40.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
|
|
|
|||
|
|
@ -15,7 +15,25 @@ class SignupWithEmailView extends StatelessWidget {
|
|||
return Form(
|
||||
key: controller.formKey,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(),
|
||||
appBar: AppBar(
|
||||
title: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 450,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
BackButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 40.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
|
|
|
|||
|
|
@ -125,9 +125,6 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
final Duration transitionAnimationDuration =
|
||||
const Duration(milliseconds: 300);
|
||||
|
||||
final Offset _defaultMessageOffset =
|
||||
const Offset(Avatar.defaultSize + 16 + 8, 300);
|
||||
|
||||
double get _horizontalPadding =>
|
||||
FluffyThemes.isColumnMode(context) ? 8.0 : 0.0;
|
||||
|
||||
|
|
@ -232,14 +229,14 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
null,
|
||||
);
|
||||
|
||||
Offset get _originalMessageOffset {
|
||||
Offset? get _originalMessageOffset {
|
||||
if (_messageRenderBox == null || !_messageRenderBox!.hasSize) {
|
||||
return _defaultMessageOffset;
|
||||
return null;
|
||||
}
|
||||
return _runWithLogging(
|
||||
() => _messageRenderBox?.localToGlobal(Offset.zero),
|
||||
"Error getting message offset",
|
||||
_defaultMessageOffset,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -267,27 +264,36 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
double? get messageLeftOffset {
|
||||
if (ownMessage) return null;
|
||||
|
||||
final offset = _originalMessageOffset;
|
||||
if (offset == null) {
|
||||
return Avatar.defaultSize + 16;
|
||||
}
|
||||
|
||||
if (isRtl) {
|
||||
return _originalMessageOffset.dx -
|
||||
(showDetails ? FluffyThemes.columnWidth : 0);
|
||||
return offset.dx - (showDetails ? FluffyThemes.columnWidth : 0);
|
||||
}
|
||||
|
||||
if (ownMessage) return null;
|
||||
return max(_originalMessageOffset.dx - columnWidth, 0);
|
||||
return max(offset.dx - columnWidth, 0);
|
||||
}
|
||||
|
||||
double? get messageRightOffset {
|
||||
if (mediaQuery == null || !ownMessage) return null;
|
||||
|
||||
final offset = _originalMessageOffset;
|
||||
if (offset == null) {
|
||||
return 8.0;
|
||||
}
|
||||
|
||||
if (isRtl) {
|
||||
return mediaQuery!.size.width -
|
||||
columnWidth -
|
||||
_originalMessageOffset.dx -
|
||||
offset.dx -
|
||||
originalMessageSize.width;
|
||||
}
|
||||
|
||||
return mediaQuery!.size.width -
|
||||
_originalMessageOffset.dx -
|
||||
offset.dx -
|
||||
originalMessageSize.width -
|
||||
(showDetails ? FluffyThemes.columnWidth : 0);
|
||||
}
|
||||
|
|
@ -344,7 +350,10 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
|
||||
bool get _hasFooterOverflow {
|
||||
if (_screenHeight == null) return false;
|
||||
final bottomOffset = _originalMessageOffset.dy +
|
||||
final offset = _originalMessageOffset;
|
||||
if (offset == null) return false;
|
||||
|
||||
final bottomOffset = offset.dy +
|
||||
originalMessageSize.height +
|
||||
_reactionsHeight +
|
||||
AppConfig.toolbarMenuHeight +
|
||||
|
|
@ -357,6 +366,8 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
double get spaceBelowContent {
|
||||
if (shouldScroll) return 0;
|
||||
if (_hasFooterOverflow) return 0;
|
||||
final offset = _originalMessageOffset;
|
||||
if (offset == null) return 300;
|
||||
|
||||
final messageHeight = originalMessageSize.height;
|
||||
final originalContentHeight =
|
||||
|
|
@ -364,8 +375,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
|
||||
final screenHeight = mediaQuery!.size.height - mediaQuery!.padding.bottom;
|
||||
|
||||
double boxHeight =
|
||||
screenHeight - _originalMessageOffset.dy - originalContentHeight;
|
||||
double boxHeight = screenHeight - offset.dy - originalContentHeight;
|
||||
|
||||
final neededSpace =
|
||||
boxHeight + _fullContentHeight + mediaQuery!.padding.top + 4.0;
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ class NewVocabBubble extends StatelessWidget {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.toys_and_games,
|
||||
Symbols.dictionary,
|
||||
color: theme.colorScheme.primary,
|
||||
size: 24,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -15,9 +15,11 @@ import 'package:fluffychat/pages/chat/chat.dart';
|
|||
import 'package:fluffychat/pages/chat/events/audio_player.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/shimmer_background.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/events/utils/report_message.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/text_to_speech/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/message_practice/message_audio_card.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/message_selection_overlay.dart';
|
||||
|
|
@ -214,6 +216,9 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
}
|
||||
|
||||
if (updatedMode == SelectMode.translate) {
|
||||
if (!InstructionsEnum.shimmerTranslation.isToggledOff) {
|
||||
InstructionsEnum.shimmerTranslation.setToggledOff(true);
|
||||
}
|
||||
await controller.fetchTranslation();
|
||||
}
|
||||
|
||||
|
|
@ -423,25 +428,32 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
colorFactor:
|
||||
theme.brightness == Brightness.light ? 0.55 : 0.3,
|
||||
builder: (context, depressed, shadowColor) =>
|
||||
AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
height: buttonSize,
|
||||
width: buttonSize,
|
||||
decoration: BoxDecoration(
|
||||
color: depressed
|
||||
? shadowColor
|
||||
: theme.colorScheme.primaryContainer,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _isPlayingNotifier,
|
||||
builder: (context, playing, __) =>
|
||||
_SelectModeButtonIcon(
|
||||
mode: mode,
|
||||
loading: controller.isLoading &&
|
||||
mode == selectedMode,
|
||||
playing: mode == SelectMode.audio && playing,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
ShimmerBackground(
|
||||
enabled: !InstructionsEnum
|
||||
.shimmerTranslation.isToggledOff &&
|
||||
mode == SelectMode.translate &&
|
||||
enabled,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
child: AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
height: buttonSize,
|
||||
width: buttonSize,
|
||||
decoration: BoxDecoration(
|
||||
color: depressed
|
||||
? shadowColor
|
||||
: theme.colorScheme.primaryContainer,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _isPlayingNotifier,
|
||||
builder: (context, playing, __) =>
|
||||
_SelectModeButtonIcon(
|
||||
mode: mode,
|
||||
loading: controller.isLoading &&
|
||||
mode == selectedMode,
|
||||
playing: mode == SelectMode.audio && playing,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
|||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/reading_assistance/tokens_util.dart';
|
||||
|
||||
mixin TokenRenderingMixin {
|
||||
|
|
@ -15,6 +16,10 @@ mixin TokenRenderingMixin {
|
|||
String? eventId,
|
||||
}) async {
|
||||
TokensUtil.collectToken(cacheKey, token.text);
|
||||
if (!InstructionsEnum.shimmerNewToken.isToggledOff) {
|
||||
InstructionsEnum.shimmerNewToken.setToggledOff(true);
|
||||
}
|
||||
|
||||
final constructs = [
|
||||
OneConstructUse(
|
||||
useType: ConstructUseTypeEnum.click,
|
||||
|
|
|
|||
19
lib/pangea/user/user_search_extension.dart
Normal file
19
lib/pangea/user/user_search_extension.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
|
||||
extension UserSearchExtension on Client {
|
||||
Future<SearchUserDirectoryResponse> searchUser(
|
||||
String search, {
|
||||
int? limit,
|
||||
}) async {
|
||||
String searchText = search;
|
||||
if (!searchText.startsWith("@")) {
|
||||
searchText = "@$searchText";
|
||||
}
|
||||
if (!searchText.contains(":")) {
|
||||
searchText = "$searchText:${Environment.homeServer}";
|
||||
}
|
||||
return searchUserDirectory(searchText, limit: limit);
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,9 @@ class ErrorReporter {
|
|||
content: Text(
|
||||
l10n.oopsSomethingWentWrong, // Use the non-null L10n instance to get the error message
|
||||
),
|
||||
// #Pangea
|
||||
showCloseIcon: true,
|
||||
// Pangea#
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue