feat: grammar practice
This commit is contained in:
parent
3be47ab6b0
commit
b698e2e84f
28 changed files with 573 additions and 303 deletions
|
|
@ -38,6 +38,7 @@ import 'package:fluffychat/pangea/analytics_misc/analytics_navigation_util.dart'
|
|||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_page/activity_archive.dart';
|
||||
import 'package:fluffychat/pangea/analytics_page/empty_analytics_page.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_page.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/level_analytics_details_content.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/pages/edit_course.dart';
|
||||
|
|
@ -59,7 +60,6 @@ import 'package:fluffychat/pangea/login/pages/signup.dart';
|
|||
import 'package:fluffychat/pangea/space_analytics/space_analytics.dart';
|
||||
import 'package:fluffychat/pangea/spaces/space_constants.dart';
|
||||
import 'package:fluffychat/pangea/subscription/pages/settings_subscription.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_page.dart';
|
||||
import 'package:fluffychat/widgets/config_viewer.dart';
|
||||
import 'package:fluffychat/widgets/layouts/empty_page.dart';
|
||||
import 'package:fluffychat/widgets/layouts/two_column_layout.dart';
|
||||
|
|
@ -542,6 +542,18 @@ abstract class AppRoutes {
|
|||
),
|
||||
redirect: loggedOutRedirect,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'practice',
|
||||
pageBuilder: (context, state) {
|
||||
return defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const AnalyticsPractice(
|
||||
type: ConstructTypeEnum.morph,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: ':construct',
|
||||
pageBuilder: (context, state) {
|
||||
|
|
@ -580,7 +592,9 @@ abstract class AppRoutes {
|
|||
return defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const VocabPractice(),
|
||||
const AnalyticsPractice(
|
||||
type: ConstructTypeEnum.vocab,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -5046,5 +5046,9 @@
|
|||
"voice": "Voice",
|
||||
"youLeftTheChat": "🚪 You left the chat",
|
||||
"downloadInitiated": "Download initiated",
|
||||
"webDownloadPermissionMessage": "If your browser blocks downloads, please enable downloads for this site."
|
||||
"webDownloadPermissionMessage": "If your browser blocks downloads, please enable downloads for this site.",
|
||||
"practiceGrammar": "Practice grammar",
|
||||
"notEnoughToPractice": "Send more messages to unlock practice",
|
||||
"constructUseCorGCDesc": "Correct grammar category practice",
|
||||
"constructUseIncGCDesc": "Incorrect grammar category practice"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,72 +183,66 @@ class ConstructAnalyticsViewState extends State<ConstructAnalyticsView> {
|
|||
),
|
||||
),
|
||||
floatingActionButton:
|
||||
widget.view == ConstructTypeEnum.vocab && widget.construct == null
|
||||
? _buildVocabPracticeButton(context)
|
||||
: null,
|
||||
widget.construct == null ? _PracticeButton(view: widget.view) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildVocabPracticeButton(BuildContext context) {
|
||||
// Check if analytics is loaded first
|
||||
if (MatrixState
|
||||
.pangeaController.matrixState.analyticsDataService.isInitializing) {
|
||||
class _PracticeButton extends StatelessWidget {
|
||||
final ConstructTypeEnum view;
|
||||
const _PracticeButton({required this.view});
|
||||
|
||||
void _showSnackbar(BuildContext context, String message) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final analyticsService = Matrix.of(context).analyticsDataService;
|
||||
if (analyticsService.isInitializing) {
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: () => _showSnackbar(
|
||||
context,
|
||||
L10n.of(context).loadingPleaseWait,
|
||||
),
|
||||
label: Text(view.practiceButtonText(context)),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
);
|
||||
}
|
||||
|
||||
final count = analyticsService.numConstructs(view);
|
||||
final enabled = count >= 10;
|
||||
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Loading vocabulary data...',
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
label: Text(L10n.of(context).practiceVocab),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
onPressed: enabled
|
||||
? () => context.go("/rooms/analytics/${view.name}/practice")
|
||||
: () => _showSnackbar(
|
||||
context,
|
||||
L10n.of(context).notEnoughToPractice,
|
||||
),
|
||||
backgroundColor:
|
||||
enabled ? null : Theme.of(context).colorScheme.surfaceContainer,
|
||||
foregroundColor: enabled
|
||||
? null
|
||||
: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!enabled) ...[
|
||||
const Icon(Icons.lock_outline, size: 18),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Text(view.practiceButtonText(context)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final vocabCount = MatrixState
|
||||
.pangeaController.matrixState.analyticsDataService
|
||||
.numConstructs(ConstructTypeEnum.vocab);
|
||||
final hasEnoughVocab = vocabCount >= 10;
|
||||
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: hasEnoughVocab
|
||||
? () {
|
||||
context.go(
|
||||
"/rooms/analytics/${ConstructTypeEnum.vocab.name}/practice",
|
||||
);
|
||||
}
|
||||
: () {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
L10n.of(context).mustHave10Words,
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor:
|
||||
hasEnoughVocab ? null : Theme.of(context).colorScheme.surfaceContainer,
|
||||
foregroundColor: hasEnoughVocab
|
||||
? null
|
||||
: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!hasEnoughVocab) ...[
|
||||
const Icon(Icons.lock_outline, size: 18),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Text(L10n.of(context).practiceVocab),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ class MorphAnalyticsListView extends StatelessWidget {
|
|||
childCount: controller.features.length,
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 75.0)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -4,20 +4,15 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/parts_of_speech_enum.dart';
|
||||
|
||||
enum ConstructTypeEnum {
|
||||
/// for vocabulary words
|
||||
vocab,
|
||||
|
||||
/// for morphs, actually called "Grammar" in the UI... :P
|
||||
morph,
|
||||
}
|
||||
morph;
|
||||
|
||||
extension ConstructExtension on ConstructTypeEnum {
|
||||
String get string {
|
||||
switch (this) {
|
||||
case ConstructTypeEnum.vocab:
|
||||
|
|
@ -37,25 +32,6 @@ extension ConstructExtension on ConstructTypeEnum {
|
|||
}
|
||||
}
|
||||
|
||||
int get maxXPPerLemma {
|
||||
switch (this) {
|
||||
case ConstructTypeEnum.vocab:
|
||||
return AnalyticsConstants.vocabUseMaxXP;
|
||||
case ConstructTypeEnum.morph:
|
||||
return AnalyticsConstants.morphUseMaxXP;
|
||||
}
|
||||
}
|
||||
|
||||
String? getDisplayCopy(String category, BuildContext context) {
|
||||
switch (this) {
|
||||
case ConstructTypeEnum.morph:
|
||||
return MorphFeaturesEnumExtension.fromString(category)
|
||||
.getDisplayCopy(context);
|
||||
case ConstructTypeEnum.vocab:
|
||||
return getVocabCategoryName(category, context);
|
||||
}
|
||||
}
|
||||
|
||||
ProgressIndicatorEnum get indicator {
|
||||
switch (this) {
|
||||
case ConstructTypeEnum.morph:
|
||||
|
|
@ -64,9 +40,17 @@ extension ConstructExtension on ConstructTypeEnum {
|
|||
return ProgressIndicatorEnum.wordsUsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ConstructTypeUtil {
|
||||
String practiceButtonText(BuildContext context) {
|
||||
final l10n = L10n.of(context);
|
||||
switch (this) {
|
||||
case ConstructTypeEnum.vocab:
|
||||
return l10n.practiceVocab;
|
||||
case ConstructTypeEnum.morph:
|
||||
return l10n.practiceGrammar;
|
||||
}
|
||||
}
|
||||
|
||||
static ConstructTypeEnum fromString(String? string) {
|
||||
switch (string) {
|
||||
case 'v':
|
||||
|
|
|
|||
|
|
@ -82,6 +82,10 @@ enum ConstructUseTypeEnum {
|
|||
// vocab lemma audio activity
|
||||
corLA,
|
||||
incLA,
|
||||
|
||||
// grammar category activity
|
||||
corGC,
|
||||
incGC,
|
||||
}
|
||||
|
||||
extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
||||
|
|
@ -163,6 +167,10 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
return L10n.of(context).constructUseCorLADesc;
|
||||
case ConstructUseTypeEnum.incLA:
|
||||
return L10n.of(context).constructUseIncLADesc;
|
||||
case ConstructUseTypeEnum.corGC:
|
||||
return L10n.of(context).constructUseCorGCDesc;
|
||||
case ConstructUseTypeEnum.incGC:
|
||||
return L10n.of(context).constructUseIncGCDesc;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -203,6 +211,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.corM:
|
||||
case ConstructUseTypeEnum.incM:
|
||||
case ConstructUseTypeEnum.ignM:
|
||||
case ConstructUseTypeEnum.corGC:
|
||||
case ConstructUseTypeEnum.incGC:
|
||||
return ActivityTypeEnum.morphId.icon;
|
||||
case ConstructUseTypeEnum.em:
|
||||
return ActivityTypeEnum.emoji.icon;
|
||||
|
|
@ -235,6 +245,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.corM:
|
||||
case ConstructUseTypeEnum.corLM:
|
||||
case ConstructUseTypeEnum.corLA:
|
||||
case ConstructUseTypeEnum.corGC:
|
||||
return 5;
|
||||
|
||||
case ConstructUseTypeEnum.pvm:
|
||||
|
|
@ -275,6 +286,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.incM:
|
||||
case ConstructUseTypeEnum.incLM:
|
||||
case ConstructUseTypeEnum.incLA:
|
||||
case ConstructUseTypeEnum.incGC:
|
||||
return -1;
|
||||
|
||||
case ConstructUseTypeEnum.incPA:
|
||||
|
|
@ -326,6 +338,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.corLA:
|
||||
case ConstructUseTypeEnum.incLA:
|
||||
case ConstructUseTypeEnum.bonus:
|
||||
case ConstructUseTypeEnum.corGC:
|
||||
case ConstructUseTypeEnum.incGC:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -369,6 +383,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.click:
|
||||
case ConstructUseTypeEnum.corLM:
|
||||
case ConstructUseTypeEnum.incLM:
|
||||
case ConstructUseTypeEnum.corGC:
|
||||
case ConstructUseTypeEnum.incGC:
|
||||
return LearningSkillsEnum.reading;
|
||||
case ConstructUseTypeEnum.pvm:
|
||||
return LearningSkillsEnum.speaking;
|
||||
|
|
@ -398,6 +414,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.corMM:
|
||||
case ConstructUseTypeEnum.corLM:
|
||||
case ConstructUseTypeEnum.corLA:
|
||||
case ConstructUseTypeEnum.corGC:
|
||||
return SpaceAnalyticsSummaryEnum.numChoicesCorrect;
|
||||
|
||||
case ConstructUseTypeEnum.incIt:
|
||||
|
|
@ -410,6 +427,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.incMM:
|
||||
case ConstructUseTypeEnum.incLM:
|
||||
case ConstructUseTypeEnum.incLA:
|
||||
case ConstructUseTypeEnum.incGC:
|
||||
return SpaceAnalyticsSummaryEnum.numChoicesIncorrect;
|
||||
|
||||
case ConstructUseTypeEnum.ignIt:
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ class OneConstructUse {
|
|||
debugger(when: kDebugMode && json['constructType'] == null);
|
||||
|
||||
final ConstructTypeEnum constructType = json['constructType'] != null
|
||||
? ConstructTypeUtil.fromString(json['constructType'])
|
||||
? ConstructTypeEnum.fromString(json['constructType'])
|
||||
: ConstructTypeEnum.vocab;
|
||||
|
||||
final useType = ConstructUseTypeUtil.fromString(json['useType']);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/analytics_details_popup/vocab_analytics_list_tile.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/analytics_navigation_util.dart';
|
||||
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/constructs/construct_identifier.dart';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
class VocabPracticeConstants {
|
||||
class AnalyticsPracticeConstants {
|
||||
static const int timeForBonus = 60;
|
||||
static const int practiceGroupSize = 10;
|
||||
}
|
||||
|
|
@ -13,6 +13,9 @@ 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/analytics_misc/example_message_util.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_repo.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_view.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/async_state.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_repo.dart';
|
||||
|
|
@ -22,47 +25,52 @@ import 'package:fluffychat/pangea/practice_activities/multiple_choice_activity_m
|
|||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_generation_repo.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_session_model.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_session_repo.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_view.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class VocabPracticeChoice {
|
||||
class PracticeChoice {
|
||||
final String choiceId;
|
||||
final String choiceText;
|
||||
final String? choiceEmoji;
|
||||
|
||||
const VocabPracticeChoice({
|
||||
const PracticeChoice({
|
||||
required this.choiceId,
|
||||
required this.choiceText,
|
||||
this.choiceEmoji,
|
||||
});
|
||||
}
|
||||
|
||||
class SessionLoader extends AsyncLoader<VocabPracticeSessionModel> {
|
||||
@override
|
||||
Future<VocabPracticeSessionModel> fetch() => VocabPracticeSessionRepo.get();
|
||||
}
|
||||
|
||||
class VocabPractice extends StatefulWidget {
|
||||
const VocabPractice({super.key});
|
||||
class SessionLoader extends AsyncLoader<AnalyticsPracticeSessionModel> {
|
||||
final ConstructTypeEnum type;
|
||||
SessionLoader({required this.type});
|
||||
|
||||
@override
|
||||
VocabPracticeState createState() => VocabPracticeState();
|
||||
Future<AnalyticsPracticeSessionModel> fetch() =>
|
||||
AnalyticsPracticeSessionRepo.get(type);
|
||||
}
|
||||
|
||||
class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
||||
final SessionLoader _sessionLoader = SessionLoader();
|
||||
class AnalyticsPractice extends StatefulWidget {
|
||||
final ConstructTypeEnum type;
|
||||
const AnalyticsPractice({
|
||||
super.key,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
@override
|
||||
AnalyticsPracticeState createState() => AnalyticsPracticeState();
|
||||
}
|
||||
|
||||
class AnalyticsPracticeState extends State<AnalyticsPractice>
|
||||
with AnalyticsUpdater {
|
||||
late final SessionLoader _sessionLoader;
|
||||
|
||||
final ValueNotifier<AsyncState<PracticeActivityModel>> activityState =
|
||||
ValueNotifier(const AsyncState.idle());
|
||||
|
||||
final Queue<MapEntry<ConstructIdentifier, Completer<PracticeActivityModel>>>
|
||||
_queue = Queue();
|
||||
final Queue<MapEntry<String, Completer<PracticeActivityModel>>> _queue =
|
||||
Queue();
|
||||
|
||||
final ValueNotifier<ConstructIdentifier?> activityConstructId =
|
||||
ValueNotifier<ConstructIdentifier?>(null);
|
||||
final ValueNotifier<String?> activityText = ValueNotifier<String?>(null);
|
||||
|
||||
final ValueNotifier<double> progressNotifier = ValueNotifier<double>(0.0);
|
||||
|
||||
|
|
@ -74,6 +82,7 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_sessionLoader = SessionLoader(type: widget.type);
|
||||
_startSession();
|
||||
_languageStreamSubscription = MatrixState
|
||||
.pangeaController.userController.languageStream.stream
|
||||
|
|
@ -84,13 +93,13 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
void dispose() {
|
||||
_languageStreamSubscription?.cancel();
|
||||
if (_isComplete) {
|
||||
VocabPracticeSessionRepo.clear();
|
||||
AnalyticsPracticeSessionRepo.clear();
|
||||
} else {
|
||||
_saveSession();
|
||||
}
|
||||
_sessionLoader.dispose();
|
||||
activityState.dispose();
|
||||
activityConstructId.dispose();
|
||||
activityText.dispose();
|
||||
progressNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
|
@ -102,19 +111,19 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
|
||||
bool get _isComplete => _sessionLoader.value?.isComplete ?? false;
|
||||
|
||||
ValueNotifier<AsyncState<VocabPracticeSessionModel>> get sessionState =>
|
||||
ValueNotifier<AsyncState<AnalyticsPracticeSessionModel>> get sessionState =>
|
||||
_sessionLoader.state;
|
||||
|
||||
AnalyticsDataService get _analyticsService =>
|
||||
Matrix.of(context).analyticsDataService;
|
||||
|
||||
List<VocabPracticeChoice> filteredChoices(
|
||||
List<PracticeChoice> filteredChoices(
|
||||
PracticeTarget target,
|
||||
MultipleChoiceActivity activity,
|
||||
) {
|
||||
final choices = activity.choices.toList();
|
||||
final answer = activity.answers.first;
|
||||
final filtered = <VocabPracticeChoice>[];
|
||||
final filtered = <PracticeChoice>[];
|
||||
|
||||
final seenTexts = <String>{};
|
||||
for (final id in choices) {
|
||||
|
|
@ -129,7 +138,7 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
(choice) => choice.choiceText == text,
|
||||
);
|
||||
if (index != -1) {
|
||||
filtered[index] = VocabPracticeChoice(
|
||||
filtered[index] = PracticeChoice(
|
||||
choiceId: id,
|
||||
choiceText: text,
|
||||
choiceEmoji: getChoiceEmoji(target, id),
|
||||
|
|
@ -140,7 +149,7 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
|
||||
seenTexts.add(text);
|
||||
filtered.add(
|
||||
VocabPracticeChoice(
|
||||
PracticeChoice(
|
||||
choiceId: id,
|
||||
choiceText: text,
|
||||
choiceEmoji: getChoiceEmoji(target, id),
|
||||
|
|
@ -164,11 +173,11 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
_choiceEmojis[target]?[choiceId];
|
||||
|
||||
String choiceTargetId(String choiceId) =>
|
||||
'vocab-choice-card-${choiceId.replaceAll(' ', '_')}';
|
||||
'${widget.type.name}-choice-card-${choiceId.replaceAll(' ', '_')}';
|
||||
|
||||
void _resetActivityState() {
|
||||
activityState.value = const AsyncState.loading();
|
||||
activityConstructId.value = null;
|
||||
activityText.value = null;
|
||||
}
|
||||
|
||||
void _resetSessionState() {
|
||||
|
|
@ -187,7 +196,10 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
|
||||
Future<void> _saveSession() async {
|
||||
if (_sessionLoader.isLoaded) {
|
||||
await VocabPracticeSessionRepo.update(_sessionLoader.value!);
|
||||
await AnalyticsPracticeSessionRepo.update(
|
||||
widget.type,
|
||||
_sessionLoader.value!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -225,7 +237,7 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
Future<void> reloadSession() async {
|
||||
_resetActivityState();
|
||||
_resetSessionState();
|
||||
await VocabPracticeSessionRepo.clear();
|
||||
await AnalyticsPracticeSessionRepo.clear();
|
||||
_sessionLoader.reset();
|
||||
await _startSession();
|
||||
}
|
||||
|
|
@ -253,7 +265,7 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
} else {
|
||||
activityState.value = const AsyncState.loading();
|
||||
final nextActivityCompleter = _queue.removeFirst();
|
||||
activityConstructId.value = nextActivityCompleter.key;
|
||||
activityText.value = nextActivityCompleter.key;
|
||||
final activity = await nextActivityCompleter.value.future;
|
||||
activityState.value = AsyncState.loaded(activity);
|
||||
}
|
||||
|
|
@ -277,7 +289,7 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
final res = await _fetchActivity(req);
|
||||
if (!mounted) return;
|
||||
|
||||
activityConstructId.value = req.targetTokens.first.vocabConstructID;
|
||||
activityText.value = req.activityText;
|
||||
activityState.value = AsyncState.loaded(res);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
|
|
@ -293,7 +305,7 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
final completer = Completer<PracticeActivityModel>();
|
||||
_queue.add(
|
||||
MapEntry(
|
||||
request.targetTokens.first.vocabConstructID,
|
||||
request.activityText,
|
||||
completer,
|
||||
),
|
||||
);
|
||||
|
|
@ -375,14 +387,14 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
|
||||
final use = OneConstructUse(
|
||||
useType: useType,
|
||||
constructType: ConstructTypeEnum.vocab,
|
||||
constructType: widget.type,
|
||||
metadata: ConstructUseMetaData(
|
||||
roomId: null,
|
||||
timeStamp: DateTime.now(),
|
||||
),
|
||||
category: activity.targetTokens.first.pos,
|
||||
lemma: activity.targetTokens.first.lemma.text,
|
||||
form: activity.targetTokens.first.lemma.text,
|
||||
category: activity.useCategory,
|
||||
lemma: activity.useLemma,
|
||||
form: activity.useForm,
|
||||
xp: useType.pointValue,
|
||||
);
|
||||
|
||||
|
|
@ -417,5 +429,5 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
_analyticsService.derivedData;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => VocabPracticeView(this);
|
||||
Widget build(BuildContext context) => AnalyticsPracticeView(this);
|
||||
}
|
||||
|
|
@ -2,12 +2,11 @@ import 'dart:math';
|
|||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_constants.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_constants.dart';
|
||||
|
||||
class VocabPracticeSessionModel {
|
||||
class AnalyticsPracticeSessionModel {
|
||||
final DateTime startedAt;
|
||||
final List<PracticeTarget> practiceTargets;
|
||||
final String userL1;
|
||||
|
|
@ -15,22 +14,16 @@ class VocabPracticeSessionModel {
|
|||
|
||||
VocabPracticeSessionState state;
|
||||
|
||||
VocabPracticeSessionModel({
|
||||
AnalyticsPracticeSessionModel({
|
||||
required this.startedAt,
|
||||
required this.practiceTargets,
|
||||
required this.userL1,
|
||||
required this.userL2,
|
||||
VocabPracticeSessionState? state,
|
||||
}) : assert(
|
||||
practiceTargets.every(
|
||||
(t) => {ActivityTypeEnum.lemmaMeaning, ActivityTypeEnum.lemmaAudio}
|
||||
.contains(t.activityType),
|
||||
),
|
||||
),
|
||||
state = state ?? const VocabPracticeSessionState();
|
||||
}) : state = state ?? const VocabPracticeSessionState();
|
||||
|
||||
int get _availableActivities => min(
|
||||
VocabPracticeConstants.practiceGroupSize,
|
||||
AnalyticsPracticeConstants.practiceGroupSize,
|
||||
practiceTargets.length,
|
||||
);
|
||||
|
||||
|
|
@ -47,7 +40,7 @@ class VocabPracticeSessionModel {
|
|||
activityQualityFeedback: null,
|
||||
targetTokens: target.tokens,
|
||||
targetType: target.activityType,
|
||||
targetMorphFeature: null,
|
||||
targetMorphFeature: target.morphFeature,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
|
@ -64,8 +57,8 @@ class VocabPracticeSessionModel {
|
|||
completedUses: [...state.completedUses, use],
|
||||
);
|
||||
|
||||
factory VocabPracticeSessionModel.fromJson(Map<String, dynamic> json) {
|
||||
return VocabPracticeSessionModel(
|
||||
factory AnalyticsPracticeSessionModel.fromJson(Map<String, dynamic> json) {
|
||||
return AnalyticsPracticeSessionModel(
|
||||
startedAt: DateTime.parse(json['startedAt'] as String),
|
||||
practiceTargets: (json['practiceTargets'] as List<dynamic>)
|
||||
.map((e) => PracticeTarget.fromJson(e))
|
||||
|
|
@ -115,7 +108,7 @@ class VocabPracticeSessionState {
|
|||
bool get _giveAccuracyBonus => accuracy >= 100.0;
|
||||
|
||||
bool get _giveTimeBonus =>
|
||||
elapsedSeconds <= VocabPracticeConstants.timeForBonus;
|
||||
elapsedSeconds <= AnalyticsPracticeConstants.timeForBonus;
|
||||
|
||||
int get bonusXP => accuracyBonusXP + timeBonusXP;
|
||||
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_constants.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class AnalyticsPracticeSessionRepo {
|
||||
static final GetStorage _storage = GetStorage('practice_session');
|
||||
|
||||
static Future<AnalyticsPracticeSessionModel> get(
|
||||
ConstructTypeEnum type,
|
||||
) async {
|
||||
final cached = _getCached(type);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
final r = Random();
|
||||
final activityTypes = ActivityTypeEnum.analyticsPracticeTypes(type);
|
||||
|
||||
final types = List.generate(
|
||||
AnalyticsPracticeConstants.practiceGroupSize,
|
||||
(_) => activityTypes[r.nextInt(activityTypes.length)],
|
||||
);
|
||||
|
||||
final List<PracticeTarget> targets = [];
|
||||
|
||||
if (type == ConstructTypeEnum.vocab) {
|
||||
final constructs = await _fetchVocab();
|
||||
final targetCount = min(constructs.length, types.length);
|
||||
targets.addAll([
|
||||
for (var i = 0; i < targetCount; i++)
|
||||
PracticeTarget(
|
||||
tokens: [constructs[i].asToken],
|
||||
activityType: types[i],
|
||||
),
|
||||
]);
|
||||
} else {
|
||||
final morphs = await _fetchMorphs();
|
||||
targets.addAll([
|
||||
for (final entry in morphs.entries)
|
||||
PracticeTarget(
|
||||
tokens: [entry.key],
|
||||
activityType: types[targets.length],
|
||||
morphFeature: entry.value,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
final session = AnalyticsPracticeSessionModel(
|
||||
userL1: MatrixState.pangeaController.userController.userL1!.langCode,
|
||||
userL2: MatrixState.pangeaController.userController.userL2!.langCode,
|
||||
startedAt: DateTime.now(),
|
||||
practiceTargets: targets,
|
||||
);
|
||||
await _setCached(type, session);
|
||||
return session;
|
||||
}
|
||||
|
||||
static Future<void> update(
|
||||
ConstructTypeEnum type,
|
||||
AnalyticsPracticeSessionModel session,
|
||||
) =>
|
||||
_setCached(type, session);
|
||||
|
||||
static Future<void> clear() => _storage.erase();
|
||||
|
||||
static Future<List<ConstructIdentifier>> _fetchVocab() async {
|
||||
final constructs = await MatrixState
|
||||
.pangeaController.matrixState.analyticsDataService
|
||||
.getAggregatedConstructs(ConstructTypeEnum.vocab)
|
||||
.then((map) => map.values.toList());
|
||||
|
||||
// sort by last used descending, nulls first
|
||||
constructs.sort((a, b) {
|
||||
final dateA = a.lastUsed;
|
||||
final dateB = b.lastUsed;
|
||||
if (dateA == null && dateB == null) return 0;
|
||||
if (dateA == null) return -1;
|
||||
if (dateB == null) return 1;
|
||||
return dateA.compareTo(dateB);
|
||||
});
|
||||
|
||||
return constructs
|
||||
.where((construct) => construct.lemma.isNotEmpty)
|
||||
.take(AnalyticsPracticeConstants.practiceGroupSize)
|
||||
.map((construct) => construct.id)
|
||||
.toList();
|
||||
}
|
||||
|
||||
static Future<Map<PangeaToken, MorphFeaturesEnum>> _fetchMorphs() async {
|
||||
final constructs = await MatrixState
|
||||
.pangeaController.matrixState.analyticsDataService
|
||||
.getAggregatedConstructs(ConstructTypeEnum.morph)
|
||||
.then((map) => map.values.toList());
|
||||
|
||||
// sort by last used descending, nulls first
|
||||
constructs.sort((a, b) {
|
||||
final dateA = a.lastUsed;
|
||||
final dateB = b.lastUsed;
|
||||
if (dateA == null && dateB == null) return 0;
|
||||
if (dateA == null) return -1;
|
||||
if (dateB == null) return 1;
|
||||
return dateA.compareTo(dateB);
|
||||
});
|
||||
|
||||
final targets = <PangeaToken, MorphFeaturesEnum>{};
|
||||
final Set<String> seenForms = {};
|
||||
|
||||
for (final entry in constructs) {
|
||||
if (targets.length >= AnalyticsPracticeConstants.practiceGroupSize) {
|
||||
break;
|
||||
}
|
||||
|
||||
final feature = MorphFeaturesEnumExtension.fromString(entry.id.category);
|
||||
if (feature == MorphFeaturesEnum.Unknown) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (final use in entry.cappedUses) {
|
||||
if (targets.length >= AnalyticsPracticeConstants.practiceGroupSize) {
|
||||
break;
|
||||
}
|
||||
|
||||
final form = use.form;
|
||||
if (seenForms.contains(form) || form == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seenForms.add(form);
|
||||
final token = PangeaToken(
|
||||
lemma: Lemma(
|
||||
text: form,
|
||||
saveVocab: true,
|
||||
form: form,
|
||||
),
|
||||
text: PangeaTokenText.fromString(form),
|
||||
pos: 'other',
|
||||
morph: {feature: use.lemma},
|
||||
);
|
||||
targets[token] = feature;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
static AnalyticsPracticeSessionModel? _getCached(
|
||||
ConstructTypeEnum type,
|
||||
) {
|
||||
try {
|
||||
final entry = _storage.read(type.name);
|
||||
if (entry == null) return null;
|
||||
final json = entry as Map<String, dynamic>;
|
||||
return AnalyticsPracticeSessionModel.fromJson(json);
|
||||
} catch (e) {
|
||||
_storage.remove(type.name);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _setCached(
|
||||
ConstructTypeEnum type,
|
||||
AnalyticsPracticeSessionModel session,
|
||||
) async {
|
||||
await _storage.write(
|
||||
type.name,
|
||||
session.toJson(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,13 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_page.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/choice_cards/audio_choice_card.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/choice_cards/game_choice_card.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/choice_cards/meaning_choice_card.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/completed_activity_session_view.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/practice_timer_widget.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/animated_progress_bar.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/async_state.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
||||
|
|
@ -9,19 +16,12 @@ import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
|
|||
import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/choice_cards/audio_choice_card.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/choice_cards/game_choice_card.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/choice_cards/meaning_choice_card.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/completed_activity_session_view.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_page.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_session_model.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_timer_widget.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
|
||||
class VocabPracticeView extends StatelessWidget {
|
||||
final VocabPracticeState controller;
|
||||
class AnalyticsPracticeView extends StatelessWidget {
|
||||
final AnalyticsPracticeState controller;
|
||||
|
||||
const VocabPracticeView(this.controller, {super.key});
|
||||
const AnalyticsPracticeView(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -53,8 +53,8 @@ class VocabPracticeView extends StatelessWidget {
|
|||
ValueListenableBuilder(
|
||||
valueListenable: controller.sessionState,
|
||||
builder: (context, state, __) {
|
||||
if (state is AsyncLoaded<VocabPracticeSessionModel>) {
|
||||
return VocabTimerWidget(
|
||||
if (state is AsyncLoaded<AnalyticsPracticeSessionModel>) {
|
||||
return PracticeTimerWidget(
|
||||
key: ValueKey(state.value.startedAt),
|
||||
initialSeconds: state.value.state.elapsedSeconds,
|
||||
onTimeUpdate: controller.updateElapsedTime,
|
||||
|
|
@ -78,12 +78,12 @@ class VocabPracticeView extends StatelessWidget {
|
|||
valueListenable: controller.sessionState,
|
||||
builder: (context, state, __) {
|
||||
return switch (state) {
|
||||
AsyncError<VocabPracticeSessionModel>(:final error) =>
|
||||
AsyncError<AnalyticsPracticeSessionModel>(:final error) =>
|
||||
ErrorIndicator(message: error.toString()),
|
||||
AsyncLoaded<VocabPracticeSessionModel>(:final value) =>
|
||||
AsyncLoaded<AnalyticsPracticeSessionModel>(:final value) =>
|
||||
value.isComplete
|
||||
? CompletedActivitySessionView(state.value, controller)
|
||||
: _VocabActivityView(controller),
|
||||
: _AnalyticsActivityView(controller),
|
||||
_ => loading,
|
||||
};
|
||||
},
|
||||
|
|
@ -93,10 +93,10 @@ class VocabPracticeView extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _VocabActivityView extends StatelessWidget {
|
||||
final VocabPracticeState controller;
|
||||
class _AnalyticsActivityView extends StatelessWidget {
|
||||
final AnalyticsPracticeState controller;
|
||||
|
||||
const _VocabActivityView(
|
||||
const _AnalyticsActivityView(
|
||||
this.controller,
|
||||
);
|
||||
|
||||
|
|
@ -119,10 +119,10 @@ class _VocabActivityView extends StatelessWidget {
|
|||
Expanded(
|
||||
flex: 1,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: controller.activityConstructId,
|
||||
builder: (context, constructId, __) => constructId != null
|
||||
valueListenable: controller.activityText,
|
||||
builder: (context, text, __) => text != null
|
||||
? Text(
|
||||
constructId.lemma,
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
|
|
@ -132,19 +132,19 @@ class _VocabActivityView extends StatelessWidget {
|
|||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Center(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: controller.activityConstructId,
|
||||
builder: (context, constructId, __) => constructId != null
|
||||
? _ExampleMessageWidget(
|
||||
controller.getExampleMessage(constructId),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Expanded(
|
||||
// flex: 2,
|
||||
// child: Center(
|
||||
// child: ValueListenableBuilder(
|
||||
// valueListenable: controller.activityConstructId,
|
||||
// builder: (context, constructId, __) => constructId != null
|
||||
// ? _ExampleMessageWidget(
|
||||
// controller.getExampleMessage(constructId),
|
||||
// )
|
||||
// : const SizedBox(),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
Expanded(
|
||||
flex: 6,
|
||||
child: _ActivityChoicesWidget(controller),
|
||||
|
|
@ -199,7 +199,7 @@ class _ExampleMessageWidget extends StatelessWidget {
|
|||
}
|
||||
|
||||
class _ActivityChoicesWidget extends StatelessWidget {
|
||||
final VocabPracticeState controller;
|
||||
final AnalyticsPracticeState controller;
|
||||
|
||||
const _ActivityChoicesWidget(
|
||||
this.controller,
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/choice_cards/game_choice_card.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/word_audio_button.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/choice_cards/game_choice_card.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
/// Displays an audio button with a select label in a row layout
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_practice/choice_cards/game_choice_card.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/choice_cards/game_choice_card.dart';
|
||||
|
||||
/// Choice card for meaning activity with emoji, and alt text on flip
|
||||
class MeaningChoiceCard extends StatelessWidget {
|
||||
|
|
@ -3,18 +3,18 @@ import 'package:flutter/material.dart';
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/level_up/star_rain_widget.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_constants.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_page.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/percent_marker_bar.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/stat_card.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/animated_progress_bar.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/percent_marker_bar.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/stat_card.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_constants.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_page.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_session_model.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class CompletedActivitySessionView extends StatelessWidget {
|
||||
final VocabPracticeSessionModel session;
|
||||
final VocabPracticeState controller;
|
||||
final AnalyticsPracticeSessionModel session;
|
||||
final AnalyticsPracticeState controller;
|
||||
const CompletedActivitySessionView(
|
||||
this.session,
|
||||
this.controller, {
|
||||
|
|
@ -198,7 +198,7 @@ class TimeStarsWidget extends StatelessWidget {
|
|||
});
|
||||
|
||||
int get starCount {
|
||||
const timeForBonus = VocabPracticeConstants.timeForBonus;
|
||||
const timeForBonus = AnalyticsPracticeConstants.timeForBonus;
|
||||
if (elapsedSeconds <= timeForBonus) return 5;
|
||||
if (elapsedSeconds <= timeForBonus * 1.5) return 4;
|
||||
if (elapsedSeconds <= timeForBonus * 2) return 3;
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/morphs/default_morph_mapping.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_models.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_repo.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class MorphCategoryActivityGenerator {
|
||||
static Future<MessageActivityResponse> get(
|
||||
MessageActivityRequest req,
|
||||
) async {
|
||||
if (req.targetMorphFeature == null) {
|
||||
throw ArgumentError(
|
||||
"MorphCategoryActivityGenerator requires a targetMorphFeature",
|
||||
);
|
||||
}
|
||||
|
||||
final feature = req.targetMorphFeature!;
|
||||
final morphTag = req.targetTokens.first.getMorphTag(feature);
|
||||
if (morphTag == null) {
|
||||
throw ArgumentError(
|
||||
"Token does not have the specified morph feature",
|
||||
);
|
||||
}
|
||||
|
||||
MorphFeaturesAndTags morphs = defaultMorphMapping;
|
||||
|
||||
try {
|
||||
final resp = await MorphsRepo.get();
|
||||
morphs = resp;
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {"l2": MatrixState.pangeaController.userController.userL2},
|
||||
);
|
||||
}
|
||||
|
||||
final List<String> allTags = morphs.getDisplayTags(feature.name);
|
||||
final List<String> possibleDistractors = allTags
|
||||
.where(
|
||||
(tag) => tag.toLowerCase() != morphTag.toLowerCase() && tag != "X",
|
||||
)
|
||||
.toList();
|
||||
|
||||
possibleDistractors.shuffle();
|
||||
final choices = possibleDistractors.take(3).toSet();
|
||||
choices.add(morphTag);
|
||||
|
||||
return MessageActivityResponse(
|
||||
activity: PracticeActivityModel(
|
||||
activityType: req.targetType,
|
||||
targetTokens: [req.targetTokens.first],
|
||||
langCode: req.userL2,
|
||||
morphFeature: feature,
|
||||
multipleChoiceContent: MultipleChoiceActivity(
|
||||
choices: choices,
|
||||
answers: {morphTag},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,12 @@ import 'dart:async';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class VocabTimerWidget extends StatefulWidget {
|
||||
class PracticeTimerWidget extends StatefulWidget {
|
||||
final int initialSeconds;
|
||||
final ValueChanged<int> onTimeUpdate;
|
||||
final bool isRunning;
|
||||
|
||||
const VocabTimerWidget({
|
||||
const PracticeTimerWidget({
|
||||
required this.initialSeconds,
|
||||
required this.onTimeUpdate,
|
||||
this.isRunning = true,
|
||||
|
|
@ -15,10 +15,10 @@ class VocabTimerWidget extends StatefulWidget {
|
|||
});
|
||||
|
||||
@override
|
||||
VocabTimerWidgetState createState() => VocabTimerWidgetState();
|
||||
PracticeTimerWidgetState createState() => PracticeTimerWidgetState();
|
||||
}
|
||||
|
||||
class VocabTimerWidgetState extends State<VocabTimerWidget> {
|
||||
class PracticeTimerWidgetState extends State<PracticeTimerWidget> {
|
||||
final Stopwatch _stopwatch = Stopwatch();
|
||||
late int _initialSeconds;
|
||||
Timer? _timer;
|
||||
|
|
@ -33,7 +33,7 @@ class VocabTimerWidgetState extends State<VocabTimerWidget> {
|
|||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(VocabTimerWidget oldWidget) {
|
||||
void didUpdateWidget(PracticeTimerWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.isRunning && !widget.isRunning) {
|
||||
_stopTimer();
|
||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
|
||||
enum ActivityTypeEnum {
|
||||
|
|
@ -13,7 +14,8 @@ enum ActivityTypeEnum {
|
|||
morphId,
|
||||
messageMeaning,
|
||||
lemmaMeaning,
|
||||
lemmaAudio;
|
||||
lemmaAudio,
|
||||
grammarCategory;
|
||||
|
||||
bool get includeTTSOnClick {
|
||||
switch (this) {
|
||||
|
|
@ -27,6 +29,7 @@ enum ActivityTypeEnum {
|
|||
case ActivityTypeEnum.hiddenWordListening:
|
||||
case ActivityTypeEnum.lemmaAudio:
|
||||
case ActivityTypeEnum.lemmaMeaning:
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -62,6 +65,9 @@ enum ActivityTypeEnum {
|
|||
case 'lemma_audio':
|
||||
case 'lemmaAudio':
|
||||
return ActivityTypeEnum.lemmaAudio;
|
||||
case 'grammar_category':
|
||||
case 'grammarCategory':
|
||||
return ActivityTypeEnum.grammarCategory;
|
||||
default:
|
||||
throw Exception('Unknown activity type: $split');
|
||||
}
|
||||
|
|
@ -117,6 +123,11 @@ enum ActivityTypeEnum {
|
|||
ConstructUseTypeEnum.corLM,
|
||||
ConstructUseTypeEnum.incLM,
|
||||
];
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
return [
|
||||
ConstructUseTypeEnum.corGC,
|
||||
ConstructUseTypeEnum.incGC,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -140,6 +151,8 @@ enum ActivityTypeEnum {
|
|||
return ConstructUseTypeEnum.corLA;
|
||||
case ActivityTypeEnum.lemmaMeaning:
|
||||
return ConstructUseTypeEnum.corLM;
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
return ConstructUseTypeEnum.corGC;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -163,6 +176,8 @@ enum ActivityTypeEnum {
|
|||
return ConstructUseTypeEnum.incLA;
|
||||
case ActivityTypeEnum.lemmaMeaning:
|
||||
return ConstructUseTypeEnum.incLM;
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
return ConstructUseTypeEnum.incGC;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -182,6 +197,7 @@ enum ActivityTypeEnum {
|
|||
case ActivityTypeEnum.morphId:
|
||||
return Icons.format_shapes;
|
||||
case ActivityTypeEnum.messageMeaning:
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
return Icons.star; // TODO: Add to L10n
|
||||
}
|
||||
}
|
||||
|
|
@ -200,6 +216,7 @@ enum ActivityTypeEnum {
|
|||
case ActivityTypeEnum.messageMeaning:
|
||||
case ActivityTypeEnum.lemmaMeaning:
|
||||
case ActivityTypeEnum.lemmaAudio:
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -210,4 +227,41 @@ enum ActivityTypeEnum {
|
|||
ActivityTypeEnum.wordFocusListening,
|
||||
ActivityTypeEnum.morphId,
|
||||
];
|
||||
|
||||
static List<ActivityTypeEnum> get vocabPracticeTypes => [
|
||||
ActivityTypeEnum.lemmaMeaning,
|
||||
// ActivityTypeEnum.lemmaAudio,
|
||||
];
|
||||
|
||||
static List<ActivityTypeEnum> get grammarPracticeTypes => [
|
||||
ActivityTypeEnum.grammarCategory,
|
||||
];
|
||||
|
||||
static List<ActivityTypeEnum> analyticsPracticeTypes(
|
||||
ConstructTypeEnum constructType,
|
||||
) {
|
||||
switch (constructType) {
|
||||
case ConstructTypeEnum.vocab:
|
||||
return vocabPracticeTypes;
|
||||
case ConstructTypeEnum.morph:
|
||||
return grammarPracticeTypes;
|
||||
}
|
||||
}
|
||||
|
||||
ConstructTypeEnum get constructType {
|
||||
switch (this) {
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
case ActivityTypeEnum.emoji:
|
||||
case ActivityTypeEnum.messageMeaning:
|
||||
case ActivityTypeEnum.lemmaMeaning:
|
||||
case ActivityTypeEnum.lemmaAudio:
|
||||
return ConstructTypeEnum.vocab;
|
||||
case ActivityTypeEnum.morphId:
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
return ConstructTypeEnum.morph;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,15 @@ class MessageActivityRequest {
|
|||
}
|
||||
}
|
||||
|
||||
String get activityText {
|
||||
switch (targetType) {
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
return "${targetTokens.first.vocabConstructID.lemma}: ${targetMorphFeature!.name}";
|
||||
default:
|
||||
return targetTokens.first.vocabConstructID.lemma;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'user_l1': userL1,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
|
|
@ -47,6 +48,45 @@ class PracticeActivityModel {
|
|||
}
|
||||
}
|
||||
|
||||
String get useCategory {
|
||||
switch (activityType.constructType) {
|
||||
case ConstructTypeEnum.morph:
|
||||
assert(
|
||||
morphFeature != null,
|
||||
"morphFeature is null in PracticeActivityModel.useCategory",
|
||||
);
|
||||
return morphFeature!.name;
|
||||
case ConstructTypeEnum.vocab:
|
||||
return targetTokens.first.pos;
|
||||
}
|
||||
}
|
||||
|
||||
String get useLemma {
|
||||
switch (activityType.constructType) {
|
||||
case ConstructTypeEnum.morph:
|
||||
assert(
|
||||
morphFeature != null,
|
||||
"morphFeature is null in PracticeActivityModel.useCategory",
|
||||
);
|
||||
final tag = targetTokens.first.getMorphTag(morphFeature!);
|
||||
if (tag == null) {
|
||||
throw ("tag is null in PracticeActivityModel.useLemma");
|
||||
}
|
||||
return tag;
|
||||
case ConstructTypeEnum.vocab:
|
||||
return targetTokens.first.lemma.text;
|
||||
}
|
||||
}
|
||||
|
||||
String get useForm {
|
||||
switch (activityType.constructType) {
|
||||
case ConstructTypeEnum.morph:
|
||||
return targetTokens.first.lemma.form;
|
||||
case ConstructTypeEnum.vocab:
|
||||
return targetTokens.first.lemma.text;
|
||||
}
|
||||
}
|
||||
|
||||
PracticeTarget get practiceTarget => PracticeTarget(
|
||||
tokens: targetTokens,
|
||||
activityType: activityType,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ import 'package:async/async.dart';
|
|||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_practice/morph_category_activity_generator.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/vocab_audio_activity_generator.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/vocab_meaning_activity_generator.dart';
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/common/network/requests.dart';
|
||||
import 'package:fluffychat/pangea/common/network/urls.dart';
|
||||
|
|
@ -21,8 +24,6 @@ import 'package:fluffychat/pangea/practice_activities/message_activity_request.d
|
|||
import 'package:fluffychat/pangea/practice_activities/morph_activity_generator.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/word_focus_listening_generator.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_audio_activity_generator.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_meaning_activity_generator.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
/// Represents an item in the completion cache.
|
||||
|
|
@ -125,6 +126,8 @@ class PracticeRepo {
|
|||
return VocabMeaningActivityGenerator.get(req);
|
||||
case ActivityTypeEnum.lemmaAudio:
|
||||
return VocabAudioActivityGenerator.get(req);
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
return MorphCategoryActivityGenerator.get(req);
|
||||
case ActivityTypeEnum.morphId:
|
||||
return MorphActivityGenerator.get(req);
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_constants.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_session_model.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class VocabPracticeSessionRepo {
|
||||
static final GetStorage _storage = GetStorage('vocab_practice_session');
|
||||
|
||||
static Future<VocabPracticeSessionModel> get() async {
|
||||
final cached = _getCached();
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
final r = Random();
|
||||
final activityTypes = [
|
||||
ActivityTypeEnum.lemmaMeaning,
|
||||
//ActivityTypeEnum.lemmaAudio,
|
||||
];
|
||||
|
||||
final types = List.generate(
|
||||
VocabPracticeConstants.practiceGroupSize,
|
||||
(_) => activityTypes[r.nextInt(activityTypes.length)],
|
||||
);
|
||||
|
||||
final constructs = await _fetch();
|
||||
final targetCount = min(constructs.length, types.length);
|
||||
final targets = [
|
||||
for (var i = 0; i < targetCount; i++)
|
||||
PracticeTarget(
|
||||
tokens: [constructs[i].asToken],
|
||||
activityType: types[i],
|
||||
),
|
||||
];
|
||||
|
||||
final session = VocabPracticeSessionModel(
|
||||
userL1: MatrixState.pangeaController.userController.userL1!.langCode,
|
||||
userL2: MatrixState.pangeaController.userController.userL2!.langCode,
|
||||
startedAt: DateTime.now(),
|
||||
practiceTargets: targets,
|
||||
);
|
||||
await _setCached(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
static Future<void> update(
|
||||
VocabPracticeSessionModel session,
|
||||
) =>
|
||||
_setCached(session);
|
||||
|
||||
static Future<void> clear() => _storage.erase();
|
||||
|
||||
static Future<List<ConstructIdentifier>> _fetch() async {
|
||||
final constructs = await MatrixState
|
||||
.pangeaController.matrixState.analyticsDataService
|
||||
.getAggregatedConstructs(ConstructTypeEnum.vocab)
|
||||
.then((map) => map.values.toList());
|
||||
|
||||
// sort by last used descending, nulls first
|
||||
constructs.sort((a, b) {
|
||||
final dateA = a.lastUsed;
|
||||
final dateB = b.lastUsed;
|
||||
if (dateA == null && dateB == null) return 0;
|
||||
if (dateA == null) return -1;
|
||||
if (dateB == null) return 1;
|
||||
return dateA.compareTo(dateB);
|
||||
});
|
||||
|
||||
return constructs
|
||||
.where((construct) => construct.lemma.isNotEmpty)
|
||||
.take(VocabPracticeConstants.practiceGroupSize)
|
||||
.map((construct) => construct.id)
|
||||
.toList();
|
||||
}
|
||||
|
||||
static VocabPracticeSessionModel? _getCached() {
|
||||
final keys = List<String>.from(_storage.getKeys());
|
||||
if (keys.isEmpty) return null;
|
||||
try {
|
||||
final json = _storage.read(keys.first) as Map<String, dynamic>;
|
||||
return VocabPracticeSessionModel.fromJson(json);
|
||||
} catch (e) {
|
||||
_storage.remove(keys.first);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _setCached(VocabPracticeSessionModel session) async {
|
||||
await _storage.erase();
|
||||
await _storage.write(
|
||||
session.startedAt.toIso8601String(),
|
||||
session.toJson(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue