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