Vocab v2 (#1402)
* 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
parent
496a789030
commit
f021e3deb2
@ -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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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…
Reference in New Issue