* starting to change vocab analytics page

* couple extra details

* Add enum for lemma categories

* Set up vocab v2 card

* Adds basic lemma definition page

* Added more elements to definition page

* Add more definition page features

* Add tooltips to definition page icons

* Get forms + examples working

* Add scrolling, edit POS retrieval

* Added POS clarification to duplicate lemmas

* Add comments, minor fix to dots

* fix: dart format and remove duplicate functions

---------

Co-authored-by: wcjord <32568597+wcjord@users.noreply.github.com>
Co-authored-by: ggurdin <ggurdin@gmail.com>
Co-authored-by: ggurdin <46800240+ggurdin@users.noreply.github.com>
pull/1593/head
Kelrap 10 months ago committed by GitHub
parent 496a789030
commit f021e3deb2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -4660,6 +4660,16 @@
"publicProfileTitle": "Allow my profile to be found in search",
"publicProfileDesc": "By enabling this option, I confirm that I am of legal age in my country of residence",
"clickWordsInstructions": "Click on individual words for more activities.",
"chooseBestDefinition": "Choose the best definition",
"meaningSectionHeader": "Meaning:",
"formSectionHeader": "Forms used in chats:",
"noEmojiSelectedTooltip": "No emoji selected",
"writingExercisesTooltip": "Writing exercises",
"listeningExercisesTooltip": "Listening exercises",
"readingExercisesTooltip": "Reading exercises",
"meaningNotFound": "Meaning could not be found.",
"formsNotFound": "Forms could not be found.",
"chooseBestDefinition": "What does this word mean?",
"chooseBaseForm": "Choose the base form",
"notTheCodeError": "Sorry, that's not the code!",
"totalXP": "Total XP",

@ -2,4 +2,9 @@ class AnalyticsConstants {
static const int xpPerLevel = 500;
static const int vocabUseMaxXP = 30;
static const int morphUseMaxXP = 500;
static const int xpForGreens = 30;
static const int xpForFlower = 100;
static const String emojiForSeed = "🫛";
static const String emojiForGreen = "🌱";
static const String emojiForFlower = "🌸";
}

@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/constants/analytics_constants.dart';
enum LemmaCategoryEnum {
flowers,
greens,
seeds,
}
extension LemmaCategoryExtension on LemmaCategoryEnum {
Color get color {
switch (this) {
case LemmaCategoryEnum.flowers:
return Color.lerp(AppConfig.primaryColor, Colors.white, 0.6) ??
AppConfig.primaryColor;
case LemmaCategoryEnum.greens:
return Color.lerp(AppConfig.success, Colors.white, 0.6) ??
AppConfig.success;
case LemmaCategoryEnum.seeds:
return Color.lerp(AppConfig.gold, Colors.white, 0.6) ?? AppConfig.gold;
}
}
Color get darkColor {
switch (this) {
case LemmaCategoryEnum.flowers:
return Color.lerp(AppConfig.primaryColor, Colors.white, 0.3) ??
AppConfig.primaryColor;
case LemmaCategoryEnum.greens:
return Color.lerp(AppConfig.success, Colors.black, 0.3) ??
AppConfig.success;
case LemmaCategoryEnum.seeds:
return Color.lerp(AppConfig.gold, Colors.black, 0.3) ?? AppConfig.gold;
}
}
String get emoji {
switch (this) {
case LemmaCategoryEnum.flowers:
return AnalyticsConstants.emojiForFlower;
case LemmaCategoryEnum.greens:
return AnalyticsConstants.emojiForGreen;
case LemmaCategoryEnum.seeds:
return AnalyticsConstants.emojiForSeed;
}
}
String get xpString {
switch (this) {
case LemmaCategoryEnum.flowers:
return ">${AnalyticsConstants.xpForFlower}";
case LemmaCategoryEnum.greens:
return ">${AnalyticsConstants.xpForGreens}";
case LemmaCategoryEnum.seeds:
return "<${AnalyticsConstants.xpForGreens}";
}
}
}

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/constants/analytics_constants.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/models/analytics/construct_use_model.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
@ -28,7 +29,7 @@ class ConstructListModel {
List<OneConstructUse> get truncatedUses => _uses.take(100).toList();
/// A map of lemmas to ConstructUses, each of which contains a lemma
/// key = lemmma + constructType.string, value = ConstructUses
/// key = lemma + constructType.string, value = ConstructUses
Map<String, ConstructUses> _constructMap = {};
/// Storing this to avoid re-running the sort operation each time this needs to
@ -229,6 +230,26 @@ class ConstructListModel {
);
}
// uses where points < AnalyticConstants.xpForGreens
List<ConstructUses> get seeds => _constructList
.where(
(use) => use.points < AnalyticsConstants.xpForGreens,
)
.toList();
List<ConstructUses> get greens => _constructList
.where(
(use) =>
use.points >= AnalyticsConstants.xpForGreens &&
use.points < AnalyticsConstants.xpForFlower,
)
.toList();
List<ConstructUses> get flowers => _constructList
.where(
(use) => use.points >= AnalyticsConstants.xpForFlower,
)
.toList();
// Not storing this for now to reduce memory load
// It's only used by downloads, so doesn't need to be accessible on the fly
Map<String, List<ConstructUses>> lemmasToUses({

@ -1,3 +1,4 @@
import 'package:fluffychat/pangea/constants/analytics_constants.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
@ -64,4 +65,19 @@ class ConstructUses {
};
return json;
}
String get xpEmoji {
if (points < AnalyticsConstants.xpForGreens) {
// bean emoji
return AnalyticsConstants.emojiForSeed;
}
if (points < AnalyticsConstants.xpForFlower) {
// sprout emoji
return AnalyticsConstants.emojiForGreen;
}
// flower emoji
return AnalyticsConstants.emojiForFlower;
}
}

@ -9,12 +9,13 @@ import 'package:fluffychat/pangea/controllers/get_analytics_controller.dart';
import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart';
import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart';
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/analytics_popup/analytics_popup.dart';
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/learning_progress_bar.dart';
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/learning_settings_button.dart';
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/level_badge.dart';
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/level_bar_popup.dart';
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/morph_analytics_popup/morph_analytics_popup.dart';
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/progress_indicator.dart';
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/vocab_analytics_popup/vocab_analytics_popup.dart';
import 'package:fluffychat/widgets/matrix.dart';
/// A summary of "My Analytics" shown at the top of the chat list
@ -109,27 +110,33 @@ class LearningProgressIndicatorsState
l2: userL2?.getDisplayName(context) ?? userL2?.langCode,
),
Row(
children: ProgressIndicatorEnum.values
.where((i) => i != ProgressIndicatorEnum.level)
.map(
(indicator) => Padding(
padding: const EdgeInsets.only(left: 6),
child: ProgressIndicatorBadge(
points: uniqueLemmas(indicator),
loading: _loading,
onTap: () {
showDialog<AnalyticsPopup>(
context: context,
builder: (c) => AnalyticsPopup(
type: indicator.constructType,
),
);
},
indicator: indicator,
children: [
ProgressIndicatorBadge(
points: uniqueLemmas(ProgressIndicatorEnum.wordsUsed),
loading: _loading,
onTap: () {
showDialog<VocabAnalyticsPopup>(
context: context,
builder: (c) => const VocabAnalyticsPopup(),
);
},
indicator: ProgressIndicatorEnum.wordsUsed,
),
ProgressIndicatorBadge(
points: uniqueLemmas(ProgressIndicatorEnum.morphsUsed),
loading: _loading,
onTap: () {
showDialog<MorphAnalyticsPopup>(
context: context,
builder: (c) => MorphAnalyticsPopup(
type: ProgressIndicatorEnum
.morphsUsed.constructType,
),
),
)
.toList(),
);
},
indicator: ProgressIndicatorEnum.morphsUsed,
),
],
),
],
),

@ -6,24 +6,24 @@ import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart';
import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
import 'package:fluffychat/pangea/models/analytics/construct_use_model.dart';
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/analytics_popup/analytics_xp_tile.dart';
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/morph_analytics_popup/morph_analytics_xp_tile.dart';
import 'package:fluffychat/widgets/matrix.dart';
class AnalyticsPopup extends StatefulWidget {
class MorphAnalyticsPopup extends StatefulWidget {
final ConstructTypeEnum type;
final bool showGroups;
const AnalyticsPopup({
const MorphAnalyticsPopup({
required this.type,
this.showGroups = true,
super.key,
});
@override
AnalyticsPopupState createState() => AnalyticsPopupState();
MorphAnalyticsPopupState createState() => MorphAnalyticsPopupState();
}
class AnalyticsPopupState extends State<AnalyticsPopup> {
class MorphAnalyticsPopupState extends State<MorphAnalyticsPopup> {
String? selectedCategory;
ConstructListModel get _constructsModel =>
MatrixState.pangeaController.getAnalytics.constructListModel;

@ -20,46 +20,49 @@ class ProgressIndicatorBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Tooltip(
message: indicator.tooltip(context),
child: PressableButton(
color: Theme.of(context).colorScheme.surfaceBright,
borderRadius: BorderRadius.circular(15),
onPressed: onTap,
buttonHeight: 2.5,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
color: Theme.of(context).colorScheme.surfaceBright,
),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
size: 14,
indicator.icon,
color: indicator.color(context),
weight: 1000,
),
const SizedBox(width: 5),
!loading
? Text(
points.toString(),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: indicator.color(context),
),
)
: const SizedBox(
height: 8,
width: 8,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
return Padding(
padding: const EdgeInsets.only(left: 6),
child: Tooltip(
message: indicator.tooltip(context),
child: PressableButton(
color: Theme.of(context).colorScheme.surfaceBright,
borderRadius: BorderRadius.circular(15),
onPressed: onTap,
buttonHeight: 2.5,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
color: Theme.of(context).colorScheme.surfaceBright,
),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
size: 14,
indicator.icon,
color: indicator.color(context),
weight: 1000,
),
const SizedBox(width: 5),
!loading
? Text(
points.toString(),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: indicator.color(context),
),
)
: const SizedBox(
height: 8,
width: 8,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
),
),
],
],
),
),
),
),

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart';
import 'package:fluffychat/pangea/models/analytics/construct_use_model.dart';
import 'package:fluffychat/pangea/utils/grammar/get_grammar_copy.dart';
class ConstructUsesXPTile extends StatelessWidget {
final ConstructUses constructUses;
const ConstructUsesXPTile(
this.constructUses, {
super.key,
});
@override
Widget build(BuildContext context) {
final ProgressIndicatorEnum indicator =
constructUses.constructType == ConstructTypeEnum.morph
? ProgressIndicatorEnum.morphsUsed
: ProgressIndicatorEnum.wordsUsed;
return Tooltip(
message:
"${constructUses.points} / ${constructUses.constructType.maxXPPerLemma}",
child: ListTile(
onTap: () {},
title: Text(
constructUses.constructType == ConstructTypeEnum.morph
? getGrammarCopy(
category: constructUses.category,
lemma: constructUses.lemma,
context: context,
) ??
constructUses.lemma
: constructUses.lemma,
),
subtitle: Row(
children: [
Expanded(
child: LinearProgressIndicator(
value: constructUses.points /
constructUses.constructType.maxXPPerLemma,
minHeight: 20,
borderRadius: const BorderRadius.all(
Radius.circular(AppConfig.borderRadius),
),
color: indicator.color(context),
),
),
const SizedBox(width: 12),
Text("${constructUses.points}xp"),
],
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
),
),
);
}
}

@ -0,0 +1,310 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/constants/analytics_constants.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/enum/lemma_category_enum.dart';
import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart';
import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
import 'package:fluffychat/pangea/models/analytics/construct_use_model.dart';
import 'package:fluffychat/pangea/utils/grammar/get_grammar_copy.dart';
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/vocab_analytics_popup/vocab_definition_popup.dart';
import 'package:fluffychat/widgets/matrix.dart';
/// Displays vocab analytics, sorted into categories
/// (flowers, greens, and seeds) by points
class VocabAnalyticsPopup extends StatefulWidget {
const VocabAnalyticsPopup({
super.key,
});
@override
VocabAnalyticsPopupState createState() => VocabAnalyticsPopupState();
}
class VocabAnalyticsPopupState extends State<VocabAnalyticsPopup> {
ConstructListModel get _constructsModel =>
MatrixState.pangeaController.getAnalytics.constructListModel;
/// Sort entries alphabetically, to better detect duplicates
List<ConstructUses> get _sortedEntries {
final entries =
_constructsModel.constructList(type: ConstructTypeEnum.vocab);
entries
.sort((a, b) => a.lemma.toLowerCase().compareTo(b.lemma.toLowerCase()));
return entries;
}
/// Produces list of chips with lemma content,
/// and assigns them to flowers, greens, and seeds tiles
Widget get dialogContent {
if (_constructsModel.constructList(type: ConstructTypeEnum.vocab).isEmpty) {
return Center(child: Text(L10n.of(context).noDataFound));
}
final sortedEntries = _sortedEntries;
// Get lists of lemmas by category
final List<Widget> flowerLemmas = [];
final List<Widget> greenLemmas = [];
final List<Widget> seedLemmas = [];
for (int i = 0; i < sortedEntries.length; i++) {
final construct = sortedEntries[i];
if (construct.lemma.isEmpty) {
continue;
}
final int points = construct.points;
String? displayText;
// Check if previous or next entry has same lemma as this entry
if ((i > 0 && sortedEntries[i - 1].lemma.equals(construct.lemma)) ||
((i < sortedEntries.length - 1 &&
sortedEntries[i + 1].lemma.equals(construct.lemma)))) {
final String pos = getGrammarCopy(
category: "pos",
lemma: construct.category,
context: context,
) ??
construct.category;
displayText = "${sortedEntries[i].lemma} (${pos.toLowerCase()})";
}
// Add VocabChip for lemma to relevant widget list, followed by comma
if (points < AnalyticsConstants.xpForGreens) {
seedLemmas.add(
VocabChip(
construct: construct,
displayText: displayText,
onTap: () {
showDialog<VocabDefinitionPopup>(
context: context,
builder: (c) => VocabDefinitionPopup(
construct: construct,
type: LemmaCategoryEnum.seeds,
points: points,
),
);
},
),
);
seedLemmas.add(
const Text(
", ",
style: TextStyle(
fontSize: 15,
color: Colors.black,
),
),
);
} else if (points >= AnalyticsConstants.xpForFlower) {
flowerLemmas.add(
VocabChip(
construct: construct,
displayText: displayText,
onTap: () {
showDialog<VocabDefinitionPopup>(
context: context,
builder: (c) => VocabDefinitionPopup(
construct: construct,
type: LemmaCategoryEnum.flowers,
points: points,
),
);
},
),
);
flowerLemmas.add(
const Text(
", ",
style: TextStyle(
fontSize: 15,
color: Colors.black,
),
),
);
} else {
greenLemmas.add(
VocabChip(
construct: construct,
displayText: displayText,
onTap: () {
showDialog<VocabDefinitionPopup>(
context: context,
builder: (c) => VocabDefinitionPopup(
construct: construct,
type: LemmaCategoryEnum.greens,
points: points,
),
);
},
),
);
greenLemmas.add(
const Text(
", ",
style: TextStyle(
fontSize: 15,
color: Colors.black,
),
),
);
}
}
// Pass sorted lemmas to background tile widgets
final Widget flowers =
dialogWidget(LemmaCategoryEnum.flowers, flowerLemmas);
final Widget greens = dialogWidget(LemmaCategoryEnum.greens, greenLemmas);
final Widget seeds = dialogWidget(LemmaCategoryEnum.seeds, seedLemmas);
return ListView(
children: [flowers, greens, seeds],
);
}
/// Tile that contains flowers, greens, or seeds chips
Widget dialogWidget(LemmaCategoryEnum type, List<Widget> lemmaList) {
// Remove extraneous commas from lemmaList
if (lemmaList.isNotEmpty) {
lemmaList.removeLast();
} else {
lemmaList.add(
const Text(
"No lemmas",
style: TextStyle(
fontSize: 15,
color: Colors.black,
),
),
);
}
return Padding(
padding: const EdgeInsets.fromLTRB(20, 10, 20, 10),
child: Material(
borderRadius:
const BorderRadius.all(Radius.circular(AppConfig.borderRadius)),
color: type.color,
child: Padding(
padding: const EdgeInsets.all(
10,
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(
backgroundColor:
Theme.of(context).brightness == Brightness.light
? Colors.white
: Colors.black,
radius: 16,
child: Text(
" ${type.emoji}",
style: const TextStyle(
fontSize: 14,
),
),
),
Text(
" ${type.xpString} XP",
style: const TextStyle(
fontSize: 15,
color: Colors.black,
),
),
],
),
const SizedBox(
height: 5,
),
Wrap(
spacing: 0,
runSpacing: 0,
children: lemmaList,
),
const SizedBox(
height: 5,
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Dialog(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 400,
maxHeight: 600,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20.0),
child: Scaffold(
appBar: AppBar(
title: Text(ProgressIndicatorEnum.wordsUsed.tooltip(context)),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: Navigator.of(context).pop,
),
// TODO: add search and training buttons?
),
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: dialogContent,
),
),
),
),
);
}
}
/// A simple chip with the text of the lemma
// TODO: highlights on hover
// callback on click
// has some padding to separate from other chips
// otherwise, is very visually simple with transparent border/background/etc
class VocabChip extends StatelessWidget {
final ConstructUses construct;
final String? displayText;
final VoidCallback onTap;
const VocabChip({
super.key,
required this.construct,
required this.onTap,
this.displayText,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Text(
displayText ?? construct.lemma,
style: const TextStyle(
// Workaround to add space between text and underline
color: Colors.transparent,
shadows: [
Shadow(
color: Colors.black,
offset: Offset(0, -3),
),
],
decoration: TextDecoration.underline,
decorationStyle: TextDecorationStyle.dashed,
decorationColor: Colors.black,
decorationThickness: 1,
fontSize: 15,
),
),
);
}
}

@ -0,0 +1,583 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/constants/morph_categories_and_labels.dart';
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/enum/lemma_category_enum.dart';
import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
import 'package:fluffychat/pangea/models/analytics/construct_use_model.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/models/lemma.dart';
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/models/pangea_token_text_model.dart';
import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_repo.dart';
import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_request.dart';
import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_response.dart';
import 'package:fluffychat/pangea/utils/grammar/get_grammar_copy.dart';
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/word_audio_button.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/matrix.dart';
/// Displays information about selected lemma, and its usage
class VocabDefinitionPopup extends StatefulWidget {
final ConstructUses construct;
final LemmaCategoryEnum type;
final int points;
const VocabDefinitionPopup({
super.key,
required this.construct,
required this.type,
required this.points,
});
@override
VocabDefinitionPopupState createState() => VocabDefinitionPopupState();
}
class VocabDefinitionPopupState extends State<VocabDefinitionPopup> {
String? exampleEventID;
LemmaInfoResponse? res;
late Future<String?> definition;
String? emoji;
PangeaToken? token;
String? morphFeature;
// Lists of lemma uses for the given exercise types; true if positive XP
List<bool> writingUses = [];
List<bool> hearingUses = [];
List<bool> readingUses = [];
late Future<List<Widget>> writingExamples;
Set<String>? forms;
String? formString;
@override
void initState() {
definition = getDefinition();
writingExamples = getExamples(loadUses());
// Get possible forms of lemma
final ConstructListModel constructsModel =
MatrixState.pangeaController.getAnalytics.constructListModel;
forms = (constructsModel.lemmasToUses())[widget.construct.lemma]
?.first
.uses
.map((e) => e.form)
.whereType<String>()
.toSet();
// Save forms as string
if (forms != null) {
formString = " ";
for (final String form in forms!) {
if (form.isNotEmpty) {
formString = "${formString!}$form, ";
}
}
if (formString!.length <= 2) {
formString = null;
} else {
formString = formString!.substring(0, formString!.length - 2);
}
}
// Find selected emoji, if applicable, using PangeaToken.getEmoji
emoji = PangeaToken(
text: PangeaTokenText(
offset: 0,
content: widget.construct.lemma,
length: widget.construct.lemma.length,
),
lemma: Lemma(
text: widget.construct.lemma,
saveVocab: false,
form: widget.construct.lemma,
),
pos: widget.construct.category,
morph: {},
).getEmoji();
exampleEventID = widget.construct.uses
.firstWhereOrNull((e) => e.metadata.eventId != null)
?.metadata
.eventId;
super.initState();
}
/// Sort uses of lemma associated with writing, reading, and listening.
List<OneConstructUse> loadUses() {
final List<OneConstructUse> writingUsesDetailed = [];
for (final OneConstructUse use in widget.construct.uses) {
if (use.useType.pointValue == 0) {
continue;
}
final bool positive = use.useType.pointValue > 0;
final ConstructUseTypeEnum activityType = use.useType;
switch (activityType) {
case ConstructUseTypeEnum.wa:
case ConstructUseTypeEnum.ga:
case ConstructUseTypeEnum.unk:
case ConstructUseTypeEnum.corIt:
case ConstructUseTypeEnum.ignIt:
case ConstructUseTypeEnum.incIt:
case ConstructUseTypeEnum.corIGC:
case ConstructUseTypeEnum.ignIGC:
case ConstructUseTypeEnum.incIGC:
case ConstructUseTypeEnum.corL:
case ConstructUseTypeEnum.ignL:
case ConstructUseTypeEnum.incL:
case ConstructUseTypeEnum.corM:
case ConstructUseTypeEnum.ignM:
case ConstructUseTypeEnum.incM:
writingUses.add(positive);
writingUsesDetailed.add(use);
break;
case ConstructUseTypeEnum.corWL:
case ConstructUseTypeEnum.ignWL:
case ConstructUseTypeEnum.incWL:
case ConstructUseTypeEnum.corHWL:
case ConstructUseTypeEnum.ignHWL:
case ConstructUseTypeEnum.incHWL:
hearingUses.add(positive);
break;
case ConstructUseTypeEnum.corPA:
case ConstructUseTypeEnum.ignPA:
case ConstructUseTypeEnum.incPA:
readingUses.add(positive);
break;
default:
break;
}
}
// Save writing uses to find usage examples
return writingUsesDetailed;
}
/// Returns a wrapping row of dots - green if positive usage, red if negative
Widget getUsageDots(List<bool> uses) {
final List<Widget> dots = [];
for (final bool use in uses) {
dots.add(
Container(
width: 15.0,
height: 15.0,
decoration: BoxDecoration(
color: use ? AppConfig.success : Colors.red,
shape: BoxShape.circle,
),
),
);
}
// Clips content (and enables scrolling) if there are 5 or more rows of dots
return ConstrainedBox(
constraints: BoxConstraints(
// TODO: May need different maxWidth for android devices
maxWidth: PlatformInfos.isMobile ? 250 : 350,
maxHeight: 90,
),
child: SingleChildScrollView(
child: Wrap(
spacing: 3,
runSpacing: 5,
children: dots,
),
),
);
}
/// Get examples of messages that uses this lemma
Future<List<Widget>> getExamples(
List<OneConstructUse> writingUsesDetailed,
) async {
final Set<String> exampleText = {};
final List<Widget> examples = [];
for (final OneConstructUse use in writingUsesDetailed) {
if (use.metadata.eventId == null) {
continue;
}
final Room? room = MatrixState.pangeaController.matrixState.client
.getRoomById(use.metadata.roomId);
final Event? event = await room?.getEventById(use.metadata.eventId!);
final String? messageText = event?.text;
if (messageText != null) {
// Save text to set, to avoid duplicate entries
exampleText.add(messageText);
if (exampleText.length >= 3) {
break;
}
}
}
// Turn message text into widgets:
for (final String text in exampleText) {
examples.add(
const SizedBox(
height: 5,
),
);
examples.add(
Container(
decoration: BoxDecoration(
color: widget.type.color,
borderRadius: BorderRadius.circular(
4,
),
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 1.5,
),
child: Text(
text,
style: const TextStyle(
color: Colors.black,
),
),
),
);
}
return examples;
}
/// Fetch the meaning of the lemma
Future<String?> getDefinition() async {
final lang2 =
MatrixState.pangeaController.languageController.userL2?.langCode;
if (lang2 == null) {
debugPrint("No lang2, cannot retrieve definition");
return L10n.of(context).meaningNotFound;
}
final LemmaInfoRequest lemmaDefReq = LemmaInfoRequest(
partOfSpeech: widget.construct.category,
lemmaLang: lang2,
userL1:
MatrixState.pangeaController.languageController.userL1?.langCode ??
LanguageKeys.defaultLanguage,
lemma: widget.construct.lemma,
);
res = await LemmaInfoRepo.get(lemmaDefReq);
return res?.meaning;
}
@override
Widget build(BuildContext context) {
final Color textColor = Theme.of(context).brightness != Brightness.light
? widget.type.color
: widget.type.darkColor;
return Dialog(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 400,
maxHeight: 600,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20.0),
child: Scaffold(
appBar: AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (emoji != null)
Text(
emoji!,
),
if (emoji == null)
Tooltip(
message: L10n.of(context).noEmojiSelectedTooltip,
child: Icon(
Icons.add_reaction_outlined,
size: 25,
color: textColor.withValues(alpha: 0.7),
),
),
const SizedBox(
width: 7,
),
Text(
widget.construct.lemma,
style: TextStyle(
color: textColor,
),
),
],
),
centerTitle: true,
leading: IconButton(
icon: Icon(Icons.adaptive.arrow_back_outlined),
color: textColor,
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
onPressed: Navigator.of(context).pop,
),
actions: (exampleEventID != null)
? [
Column(
children: [
const SizedBox(height: 6),
WordAudioButton(
text: widget.construct.lemma,
ttsController: TtsController(),
eventID: exampleEventID!,
),
],
),
const SizedBox(width: 8),
]
: [],
),
body: SingleChildScrollView(
child: Padding(
padding:
const EdgeInsets.symmetric(vertical: 20, horizontal: 10),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Tooltip(
message: L10n.of(context).grammarCopyPOS,
child: Icon(
(morphFeature != null)
? getIconForMorphFeature(morphFeature!)
: Symbols.toys_and_games,
size: 23,
color: textColor.withValues(alpha: 0.7),
),
),
const SizedBox(
width: 5,
),
Text(
getGrammarCopy(
category: "pos",
lemma: widget.construct.category,
context: context,
) ??
widget.construct.category,
style: TextStyle(
color: textColor,
fontSize: 16,
),
),
],
),
const SizedBox(
height: 20,
),
Align(
alignment: Alignment.topLeft,
child: FutureBuilder(
future: definition,
builder: (
BuildContext context,
AsyncSnapshot<String?> snapshot,
) {
if (snapshot.hasData) {
return RichText(
text: TextSpan(
style: TextStyle(
color:
Theme.of(context).colorScheme.onSurface,
fontSize: 16,
),
children: <TextSpan>[
TextSpan(
text: L10n.of(context).meaningSectionHeader,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
TextSpan(text: " ${snapshot.data!}"),
],
),
);
} else {
return Wrap(
children: [
Text(
L10n.of(context).meaningSectionHeader,
style: TextStyle(
color:
Theme.of(context).colorScheme.onSurface,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(
width: 10,
),
const CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
],
);
}
},
),
),
const SizedBox(
height: 10,
),
Align(
alignment: Alignment.topLeft,
child: RichText(
text: TextSpan(
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontSize: 16,
),
children: <TextSpan>[
TextSpan(
text: L10n.of(context).formSectionHeader,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
TextSpan(
text: formString ??
" ${L10n.of(context).formsNotFound}",
),
],
),
),
),
const SizedBox(
height: 20,
),
Divider(
height: 3,
color: textColor.withValues(alpha: 0.7),
),
const SizedBox(
height: 20,
),
Text(
"${widget.type.emoji} ${widget.points} XP",
style: TextStyle(
color: textColor,
fontSize: 20,
),
),
const SizedBox(
height: 20,
),
// Writing exercise section
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Tooltip(
message: L10n.of(context).writingExercisesTooltip,
child: Icon(
Symbols.edit_square,
size: 25,
color: textColor.withValues(alpha: 0.7),
),
),
const SizedBox(
width: 7,
),
getUsageDots(writingUses),
],
),
FutureBuilder(
future: writingExamples,
builder: (
BuildContext context,
AsyncSnapshot<List<Widget>> snapshot,
) {
if (snapshot.hasData) {
return Align(
alignment: Alignment.topLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: snapshot.data!,
),
);
} else {
return const Column(
children: [
SizedBox(height: 10),
CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
],
);
}
},
),
const SizedBox(
height: 20,
),
// Listening exercise section
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Tooltip(
message: L10n.of(context).listeningExercisesTooltip,
child: Icon(
Icons.hearing,
size: 25,
color: textColor.withValues(alpha: 0.7),
),
),
const SizedBox(
width: 7,
),
getUsageDots(hearingUses),
],
),
const SizedBox(
height: 20,
),
// Reading exercise section
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Tooltip(
message: L10n.of(context).readingExercisesTooltip,
child: Icon(
Symbols.two_pager,
size: 25,
color: textColor.withValues(alpha: 0.7),
),
),
const SizedBox(
width: 7,
),
getUsageDots(readingUses),
],
),
],
),
),
),
),
),
),
);
}
}
Loading…
Cancel
Save