1719 grammar detailed view in analytics (#1728)

* feat: grammar analytics details page

---------

Co-authored-by: wcjord <32568597+wcjord@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
pull/1605/head
ggurdin 9 months ago committed by GitHub
parent 8988cce68a
commit a71f519700
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -2,8 +2,10 @@ import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/analytics_details_popup/morph_analytics_view.dart';
import 'package:fluffychat/pangea/analytics_details_popup/morph_details_view.dart';
import 'package:fluffychat/pangea/analytics_details_popup/vocab_analytics_view.dart';
import 'package:fluffychat/pangea/analytics_details_popup/vocab_details_view.dart';
import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_identifier.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart';
@ -60,26 +62,45 @@ class AnalyticsPopupWrapperState extends State<AnalyticsPopupWrapper> {
? () => Navigator.of(context).pop()
: () => setConstructZoom(null),
),
actions: ConstructTypeEnum.values
.map(
(c) => IconButton(
icon: Icon(c.indicator.icon),
onPressed: () => setState(() {
localView = c;
actions: [
Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
height: 30.0,
width: 30.0,
child: InkWell(
child: Image.network(
'${AppConfig.assetsBaseURL}/${AnalyticsConstants.vocabIconFileName}',
),
onTap: () => setState(() {
localView = ConstructTypeEnum.vocab;
localConstructZoom = null;
}),
isSelected: localView == c,
color: localView == c
? Theme.of(context).brightness == Brightness.dark
? AppConfig.primaryColorLight
: AppConfig.primaryColor
: null,
),
)
.toList(),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
height: 30.0,
width: 30.0,
child: InkWell(
child: Image.network(
'${AppConfig.assetsBaseURL}/${AnalyticsConstants.morphIconFileName}',
),
onTap: () => setState(() {
localView = ConstructTypeEnum.morph;
localConstructZoom = null;
}),
),
),
),
],
),
body: localView == ConstructTypeEnum.morph
? const MorphAnalyticsView()
? localConstructZoom == null
? MorphAnalyticsView(onConstructZoom: setConstructZoom)
: MorphDetailsView(constructId: localConstructZoom!)
: localConstructZoom == null
? VocabAnalyticsView(onConstructZoom: setConstructZoom)
: VocabDetailsView(constructId: localConstructZoom!),

@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/analytics_details_popup/lemma_usage_dots.dart';
import 'package:fluffychat/pangea/analytics_details_popup/lemma_use_example_messages.dart';
import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_identifier.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_level_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart';
class AnalyticsDetailsViewContent extends StatelessWidget {
final Widget title;
final Widget subtitle;
final Widget headerContent;
final Widget xpIcon;
final ConstructIdentifier constructId;
const AnalyticsDetailsViewContent({
required this.title,
required this.subtitle,
required this.xpIcon,
required this.headerContent,
required this.constructId,
super.key,
});
ConstructUses get construct => constructId.constructUses;
@override
Widget build(BuildContext context) {
final Color textColor = Theme.of(context).brightness != Brightness.light
? construct.lemmaCategory.color
: construct.lemmaCategory.darkColor;
return SingleChildScrollView(
child: Column(
children: [
title,
const SizedBox(height: 16.0),
subtitle,
const SizedBox(height: 16.0),
headerContent,
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Image.network(
"${AppConfig.assetsBaseURL}/${AnalyticsConstants.popupDividerFileName}",
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
xpIcon,
const SizedBox(width: 16.0),
Text(
"${construct.points} XP",
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: textColor,
),
),
],
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
LemmaUseExampleMessages(construct: construct),
// Writing exercise section
LemmaUsageDots(
construct: construct,
category: LearningSkillsEnum.writing,
tooltip: L10n.of(context).writingExercisesTooltip,
icon: Symbols.edit_square,
),
// Listening exercise section
LemmaUsageDots(
construct: construct,
category: LearningSkillsEnum.hearing,
tooltip: L10n.of(context).listeningExercisesTooltip,
icon: Symbols.hearing,
),
// Reading exercise section
LemmaUsageDots(
construct: construct,
category: LearningSkillsEnum.reading,
tooltip: L10n.of(context).readingExercisesTooltip,
icon: Symbols.two_pager,
),
],
),
),
],
),
);
}
}

@ -4,7 +4,6 @@ import 'package:collection/collection.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_level_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
@ -89,9 +88,6 @@ class LemmaUseExampleMessages extends StatelessWidget {
vertical: 8,
),
margin: const EdgeInsets.only(bottom: 8),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 1.5,
),
child: RichText(
text: TextSpan(
style: TextStyle(

@ -16,7 +16,10 @@ import 'package:fluffychat/widgets/matrix.dart';
import '../morphs/morph_repo.dart';
class MorphAnalyticsView extends StatelessWidget {
final void Function(ConstructIdentifier) onConstructZoom;
const MorphAnalyticsView({
required this.onConstructZoom,
super.key,
});
@ -42,6 +45,7 @@ class MorphAnalyticsView extends StatelessWidget {
.map((tag) => tag.toLowerCase())
.toSet() ??
{},
onConstructZoom: onConstructZoom,
)
: const SizedBox.shrink(),
)
@ -56,11 +60,13 @@ class MorphAnalyticsView extends StatelessWidget {
class MorphFeatureBox extends StatelessWidget {
final String morphFeature;
final Set<String> allTags;
final void Function(ConstructIdentifier) onConstructZoom;
const MorphFeatureBox({
super.key,
required this.morphFeature,
required this.allTags,
required this.onConstructZoom,
});
String _categoryCopy(
@ -125,25 +131,32 @@ class MorphFeatureBox extends StatelessWidget {
runSpacing: 16.0,
children: allTags
.map(
(morphTag) => MorphTagChip(
morphFeature: morphFeature,
morphTag: morphTag,
constructAnalytics: MatrixState.pangeaController
(morphTag) {
final id = ConstructIdentifier(
lemma: morphTag,
type: ConstructTypeEnum.morph,
category: morphFeature,
);
final analytics = MatrixState.pangeaController
.getAnalytics.constructListModel
.getConstructUses(
ConstructIdentifier(
lemma: morphTag,
type: ConstructTypeEnum.morph,
category: morphFeature,
),
) ??
.getConstructUses(id) ??
ConstructUses(
lemma: morphTag,
constructType: ConstructTypeEnum.morph,
category: morphFeature,
uses: [],
),
),
);
return MorphTagChip(
morphFeature: morphFeature,
morphTag: morphTag,
constructAnalytics: analytics,
onTap: analytics.points > 0
? () => onConstructZoom(id)
: null,
);
},
)
.sortedBy<num>(
(chip) => chip.constructAnalytics.points,
@ -164,72 +177,78 @@ class MorphTagChip extends StatelessWidget {
final String morphFeature;
final String morphTag;
final ConstructUses constructAnalytics;
final VoidCallback? onTap;
const MorphTagChip({
super.key,
required this.morphFeature,
required this.morphTag,
required this.constructAnalytics,
this.onTap,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Opacity(
opacity: constructAnalytics.points > 0 ? 1.0 : 0.3,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32.0),
gradient: constructAnalytics.points > 0
? LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: <Color>[
Colors.transparent,
constructAnalytics.lemmaCategory.color,
],
)
: null,
color: constructAnalytics.points > 0 ? null : theme.disabledColor,
),
padding: const EdgeInsets.symmetric(
vertical: 4.0,
horizontal: 8.0,
),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 8.0,
children: [
SizedBox(
width: 28.0,
height: 28.0,
child: constructAnalytics.points > 0 ||
Matrix.of(context).client.isSupportAccount
? MorphIcon(
morphFeature: morphFeature,
morphTag: morphTag,
)
: const Icon(
Icons.lock,
color: Colors.white,
),
),
Text(
getGrammarCopy(
category: morphFeature,
lemma: morphTag,
context: context,
) ??
morphTag,
style: TextStyle(
fontWeight: FontWeight.bold,
color: theme.brightness == Brightness.dark
? Colors.white
: Colors.black,
return InkWell(
borderRadius: BorderRadius.circular(32.0),
onTap: onTap,
child: Opacity(
opacity: constructAnalytics.points > 0 ? 1.0 : 0.3,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32.0),
gradient: constructAnalytics.points > 0
? LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: <Color>[
Colors.transparent,
constructAnalytics.lemmaCategory.color,
],
)
: null,
color: constructAnalytics.points > 0 ? null : theme.disabledColor,
),
padding: const EdgeInsets.symmetric(
vertical: 4.0,
horizontal: 8.0,
),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 8.0,
children: [
SizedBox(
width: 28.0,
height: 28.0,
child: constructAnalytics.points > 0 ||
Matrix.of(context).client.isSupportAccount
? MorphIcon(
morphFeature: morphFeature,
morphTag: morphTag,
)
: const Icon(
Icons.lock,
color: Colors.white,
),
),
Text(
getGrammarCopy(
category: morphFeature,
lemma: morphTag,
context: context,
) ??
morphTag,
style: TextStyle(
fontWeight: FontWeight.bold,
color: theme.brightness == Brightness.dark
? Colors.white
: Colors.black,
),
),
),
],
],
),
),
),
);

@ -0,0 +1,174 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup_content.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_identifier.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_level_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart';
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_repo.dart';
class MorphDetailsView extends StatelessWidget {
final ConstructIdentifier constructId;
const MorphDetailsView({
required this.constructId,
super.key,
});
ConstructUses get _construct => constructId.constructUses;
String get _morphFeature => constructId.category;
String get _morphTag => constructId.lemma;
String _categoryCopy(
BuildContext context,
) {
if (_morphFeature.toLowerCase() == "other") {
return L10n.of(context).other;
}
return ConstructTypeEnum.morph.getDisplayCopy(
_morphFeature,
context,
) ??
_morphFeature;
}
Future<String> _getDefinition(BuildContext context) => MorphInfoRepo.get(
feature: _construct.category,
tag: _construct.lemma,
).then((value) => value ?? L10n.of(context).meaningNotFound);
@override
Widget build(BuildContext context) {
final Color textColor = Theme.of(context).brightness != Brightness.light
? _construct.lemmaCategory.color
: _construct.lemmaCategory.darkColor;
return AnalyticsDetailsViewContent(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 32.0,
height: 32.0,
child: MorphIcon(
morphFeature: _morphFeature,
morphTag: _morphTag,
),
),
const SizedBox(width: 10.0),
Text(
getGrammarCopy(
category: _morphFeature,
lemma: _morphTag,
context: context,
) ??
_morphTag,
style: Theme.of(context).textTheme.titleLarge,
),
],
),
subtitle: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 24.0,
height: 24.0,
child: MorphIcon(morphFeature: _morphFeature, morphTag: null),
),
const SizedBox(width: 10.0),
Text(
_categoryCopy(context),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: textColor,
),
),
],
),
headerContent: Padding(
padding: const EdgeInsets.all(25.0),
child: Align(
alignment: Alignment.topLeft,
child: FutureBuilder(
future: _getDefinition(context),
builder: (
BuildContext context,
AsyncSnapshot<String?> snapshot,
) {
if (snapshot.hasData) {
return RichText(
text: TextSpan(
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
children: <TextSpan>[
TextSpan(
text: L10n.of(context).meaningSectionHeader,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
TextSpan(
text: " ${snapshot.data!}",
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
);
} else if (snapshot.hasError) {
return Wrap(
children: [
Text(
L10n.of(context).meaningSectionHeader,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(
width: 10,
),
Text(
L10n.of(context).meaningNotFound,
style: Theme.of(context).textTheme.bodyLarge,
),
],
);
} else {
return Wrap(
children: [
Text(
L10n.of(context).meaningSectionHeader,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(
width: 10,
),
const TextLoadingShimmer(width: 100),
],
);
}
},
),
),
),
xpIcon: CircleAvatar(
radius: 16.0,
backgroundColor: _construct.lemmaCategory.color,
child: const Icon(
Icons.star,
color: Colors.white,
size: 20.0,
),
),
constructId: constructId,
);
}
}

@ -3,13 +3,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:fluffychat/pangea/analytics_details_popup/lemma_usage_dots.dart';
import 'package:fluffychat/pangea/analytics_details_popup/lemma_use_example_messages.dart';
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup_content.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_identifier.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_level_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart';
import 'package:fluffychat/pangea/common/widgets/customized_svg.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
@ -32,29 +30,29 @@ class VocabDetailsView extends StatelessWidget {
required this.constructId,
});
ConstructUses get construct => constructId.constructUses;
ConstructUses get _construct => constructId.constructUses;
String? get emoji => PangeaToken(
String? get _emoji => PangeaToken(
text: PangeaTokenText(
offset: 0,
content: construct.lemma,
length: construct.lemma.length,
content: _construct.lemma,
length: _construct.lemma.length,
),
lemma: Lemma(
text: construct.lemma,
text: _construct.lemma,
saveVocab: false,
form: construct.lemma,
form: _construct.lemma,
),
pos: construct.category,
pos: _construct.category,
morph: {},
).getEmoji();
/// Get string representing forms of the given lemma that have been used
String? get formString {
String? get _formString {
// Get possible forms of lemma
final constructs = MatrixState
.pangeaController.getAnalytics.constructListModel
.getConstructUsesByLemma(construct.lemma);
.getConstructUsesByLemma(_construct.lemma);
final forms = constructs
.map((e) => e.uses)
@ -70,7 +68,7 @@ class VocabDetailsView extends StatelessWidget {
}
/// Fetch the meaning of the lemma
Future<String?> getDefinition(BuildContext context) async {
Future<String?> _getDefinition(BuildContext context) async {
final lang2 =
MatrixState.pangeaController.languageController.userL2?.langCode;
if (lang2 == null) {
@ -79,12 +77,12 @@ class VocabDetailsView extends StatelessWidget {
}
final LemmaInfoRequest lemmaDefReq = LemmaInfoRequest(
partOfSpeech: construct.category,
partOfSpeech: _construct.category,
lemmaLang: lang2,
userL1:
MatrixState.pangeaController.languageController.userL1?.langCode ??
LanguageKeys.defaultLanguage,
lemma: construct.lemma,
lemma: _construct.lemma,
);
final LemmaInfoResponse res = await LemmaInfoRepo.get(lemmaDefReq);
return res.meaning;
@ -93,118 +91,88 @@ class VocabDetailsView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final Color textColor = Theme.of(context).brightness != Brightness.light
? construct.lemmaCategory.color
: construct.lemmaCategory.darkColor;
? _construct.lemmaCategory.color
: _construct.lemmaCategory.darkColor;
return Scaffold(
appBar: AppBar(
leading: const SizedBox(),
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 42,
child: emoji == null
? Tooltip(
message: L10n.of(context).noEmojiSelectedTooltip,
child: Icon(
Icons.add_reaction_outlined,
size: 24,
color: textColor.withValues(alpha: 0.7),
),
)
: Text(emoji!),
),
const SizedBox(width: 10.0),
Text(
construct.lemma,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(width: 10.0),
WordAudioButton(
text: construct.lemma,
ttsController: TtsController(),
size: 24,
),
const SizedBox(width: 24),
],
),
centerTitle: true,
// leading: SizedBox(
// width: 24,
// child: BackButton(onPressed: onClose),
// ),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Tooltip(
message: L10n.of(context).grammarCopyPOS,
return AnalyticsDetailsViewContent(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 42,
child: _emoji == null
? Tooltip(
message: L10n.of(context).noEmojiSelectedTooltip,
child: Icon(
Symbols.toys_and_games,
size: 23,
Icons.add_reaction_outlined,
size: 24,
color: textColor.withValues(alpha: 0.7),
),
),
const SizedBox(width: 10.0),
Text(
getGrammarCopy(
category: "pos",
lemma: construct.category,
context: context,
) ??
construct.category,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: textColor,
),
),
],
),
const SizedBox(height: 20.0),
Padding(
padding: const EdgeInsets.all(5.0),
child: Align(
alignment: Alignment.topLeft,
child: FutureBuilder(
future: getDefinition(context),
builder: (
BuildContext context,
AsyncSnapshot<String?> snapshot,
) {
if (snapshot.hasData) {
return RichText(
text: TextSpan(
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
children: <TextSpan>[
TextSpan(
text: L10n.of(context).meaningSectionHeader,
style: Theme.of(context)
.textTheme
.bodyLarge
?.copyWith(
fontWeight: FontWeight.bold,
),
),
TextSpan(
text: " ${snapshot.data!}",
style: Theme.of(context).textTheme.bodyLarge,
),
],
)
: Text(_emoji!),
),
const SizedBox(width: 10.0),
Text(
_construct.lemma,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(width: 10.0),
WordAudioButton(
text: _construct.lemma,
ttsController: TtsController(),
size: 24,
),
],
),
subtitle: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Tooltip(
message: L10n.of(context).grammarCopyPOS,
child: Icon(
Symbols.toys_and_games,
size: 23,
color: textColor.withValues(alpha: 0.7),
),
),
const SizedBox(width: 10.0),
Text(
getGrammarCopy(
category: "pos",
lemma: _construct.category,
context: context,
) ??
_construct.category,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: textColor,
),
),
],
),
headerContent: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(5.0),
child: Align(
alignment: Alignment.topLeft,
child: FutureBuilder(
future: _getDefinition(context),
builder: (
BuildContext context,
AsyncSnapshot<String?> snapshot,
) {
if (snapshot.hasData) {
return RichText(
text: TextSpan(
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
);
} else {
return Wrap(
children: [
Text(
L10n.of(context).meaningSectionHeader,
children: <TextSpan>[
TextSpan(
text: L10n.of(context).meaningSectionHeader,
style: Theme.of(context)
.textTheme
.bodyLarge
@ -212,100 +180,73 @@ class VocabDetailsView extends StatelessWidget {
fontWeight: FontWeight.bold,
),
),
const SizedBox(
width: 10,
),
const CircularProgressIndicator.adaptive(
strokeWidth: 2,
TextSpan(
text: " ${snapshot.data!}",
style: Theme.of(context).textTheme.bodyLarge,
),
],
);
}
},
),
),
),
Padding(
padding: const EdgeInsets.all(5.0),
child: Align(
alignment: Alignment.topLeft,
child: RichText(
text: TextSpan(
style: Theme.of(context).textTheme.bodyLarge,
children: <TextSpan>[
TextSpan(
text: L10n.of(context).formSectionHeader,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
TextSpan(
text:
" ${formString ?? L10n.of(context).formsNotFound}",
),
],
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Divider(
height: 3,
color: textColor.withValues(alpha: 0.7),
);
} else {
return Wrap(
children: [
Text(
L10n.of(context).meaningSectionHeader,
style:
Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(
width: 10,
),
const CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
],
);
}
},
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(right: 10),
child: CustomizedSvg(
svgUrl: construct.lemmaCategory.svgURL,
colorReplacements: const {},
errorIcon: Text(
construct.lemmaCategory.emoji,
),
Padding(
padding: const EdgeInsets.all(5.0),
child: Align(
alignment: Alignment.topLeft,
child: RichText(
text: TextSpan(
style: Theme.of(context).textTheme.bodyLarge,
children: <TextSpan>[
TextSpan(
text: L10n.of(context).formSectionHeader,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
Text(
"${construct.points} XP",
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: textColor,
),
TextSpan(
text:
" ${_formString ?? L10n.of(context).formsNotFound}",
),
],
),
],
),
const SizedBox(height: 20),
LemmaUseExampleMessages(construct: construct),
// Writing exercise section
LemmaUsageDots(
construct: construct,
category: LearningSkillsEnum.writing,
tooltip: L10n.of(context).writingExercisesTooltip,
icon: Symbols.edit_square,
),
// Listening exercise section
LemmaUsageDots(
construct: construct,
category: LearningSkillsEnum.hearing,
tooltip: L10n.of(context).listeningExercisesTooltip,
icon: Symbols.hearing,
),
// Reading exercise section
LemmaUsageDots(
construct: construct,
category: LearningSkillsEnum.reading,
tooltip: L10n.of(context).readingExercisesTooltip,
icon: Symbols.two_pager,
),
),
],
),
],
),
),
xpIcon: CustomizedSvg(
svgUrl: _construct.lemmaCategory.svgURL,
colorReplacements: const {},
errorIcon: Text(
_construct.lemmaCategory.emoji,
style: const TextStyle(
fontSize: 20,
),
),
),
constructId: constructId,
);
}
}

@ -12,4 +12,7 @@ class AnalyticsConstants {
static const String emojiForFlower = "🌸";
static const levelUpAudioFileName = "LevelUp_chime.mp3";
static const levelUpImageFileName = "LvL_Up_Full_Banner.png";
static const popupDividerFileName = "divider.png";
static const vocabIconFileName = "Vocabulary_icon.png";
static const morphIconFileName = "grammar_icon.png";
}

@ -65,6 +65,7 @@ class PApiUrls {
"${PApiUrls.choreoEndpoint}/practice";
static String lemmaDictionary = "${PApiUrls.choreoEndpoint}/lemma_definition";
static String morphDictionary = "${PApiUrls.choreoEndpoint}/morph_meaning";
static String activityPlanGeneration =
"${PApiUrls.choreoEndpoint}/activity_plan";

@ -0,0 +1,99 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/network/requests.dart';
import 'package:fluffychat/pangea/common/network/urls.dart';
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_request.dart';
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_response.dart';
import 'package:fluffychat/widgets/matrix.dart';
class _APICallCacheItem {
final DateTime time;
final Future<MorphInfoResponse> future;
_APICallCacheItem(this.time, this.future);
}
class MorphInfoRepo {
static final GetStorage _morphMeaningStorage =
GetStorage('morph_meaning_storage');
static final shortTermCache = <String, _APICallCacheItem>{};
static const int _cacheDurationMinutes = 1;
static void set(MorphInfoRequest request, MorphInfoResponse response) {
_morphMeaningStorage.write(request.storageKey, response.toJson());
}
static Future<MorphInfoResponse> _fetch(MorphInfoRequest request) async {
try {
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: MatrixState.pangeaController.userController.accessToken,
);
final Response res = await req.post(
url: PApiUrls.morphDictionary,
body: request.toJson(),
);
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
final response = MorphInfoResponse.fromJson(decodedBody);
set(request, response);
return response;
} catch (e, s) {
debugPrint('Error fetching morph info: $e');
return Future.error(e);
}
}
static Future<MorphInfoResponse> _get(MorphInfoRequest request) async {
request.userL1 == request.userL1.split('-').first;
request.userL2 == request.userL2.split('-').first;
final cachedJson = _morphMeaningStorage.read(request.storageKey);
if (cachedJson != null) {
return MorphInfoResponse.fromJson(cachedJson);
}
final _APICallCacheItem? cachedCall = shortTermCache[request.storageKey];
if (cachedCall != null) {
if (DateTime.now().difference(cachedCall.time).inMinutes <
_cacheDurationMinutes) {
return cachedCall.future;
} else {
shortTermCache.remove(request.storageKey);
}
}
final future = _fetch(request);
shortTermCache[request.storageKey] =
_APICallCacheItem(DateTime.now(), future);
return future;
}
static Future<String?> get({
required String feature,
required String tag,
}) async {
final res = await _get(
MorphInfoRequest(
userL1:
MatrixState.pangeaController.languageController.userL1?.langCode ??
LanguageKeys.defaultLanguage,
userL2:
MatrixState.pangeaController.languageController.userL2?.langCode ??
LanguageKeys.defaultLanguage,
),
);
return res.getFeatureByCode(feature)?.getTagByCode(tag)?.l1Description;
}
}

@ -0,0 +1,30 @@
class MorphInfoRequest {
final String userL1;
final String userL2;
MorphInfoRequest({
required this.userL1,
required this.userL2,
});
Map<String, dynamic> toJson() {
return {
'user_l1': userL1,
'user_l2': userL2,
};
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MorphInfoRequest &&
userL1 == other.userL1 &&
userL2 == other.userL2;
@override
int get hashCode => userL1.hashCode ^ userL2.hashCode;
String get storageKey {
return userL1 + userL2;
}
}

@ -0,0 +1,112 @@
import 'package:collection/collection.dart';
class MorphologicalTag {
final String code;
final String l2Title;
final String l1Title;
final String l1Description;
MorphologicalTag({
required this.code,
required this.l2Title,
required this.l1Title,
required this.l1Description,
});
factory MorphologicalTag.fromJson(Map<String, dynamic> json) {
return MorphologicalTag(
code: json['code'],
l2Title: json['l2_title'],
l1Title: json['l1_title'],
l1Description: json['l1_description'],
);
}
Map<String, dynamic> toJson() {
return {
'code': code,
'l2_title': l2Title,
'l1_title': l1Title,
'l1_description': l1Description,
};
}
}
class MorphologicalFeature {
final String code;
final String l2Title;
final String l1Title;
final List<MorphologicalTag> tags;
MorphologicalFeature({
required this.code,
required this.l2Title,
required this.l1Title,
required this.tags,
});
factory MorphologicalFeature.fromJson(Map<String, dynamic> json) {
final tagsFromJson = json['tags'] as List;
final List<MorphologicalTag> tagsList =
tagsFromJson.map((tag) => MorphologicalTag.fromJson(tag)).toList();
return MorphologicalFeature(
code: json['code'],
l2Title: json['l2_title'],
l1Title: json['l1_title'],
tags: tagsList,
);
}
Map<String, dynamic> toJson() {
return {
'code': code,
'l2_title': l2Title,
'l1_title': l1Title,
'tags': tags.map((tag) => tag.toJson()).toList(),
};
}
MorphologicalTag? getTagByCode(String code) {
return tags.firstWhereOrNull(
(tag) => tag.code.toLowerCase() == code.toLowerCase());
}
}
class MorphInfoResponse {
final String userL1;
final String userL2;
final List<MorphologicalFeature> features;
MorphInfoResponse({
required this.userL1,
required this.userL2,
required this.features,
});
factory MorphInfoResponse.fromJson(Map<String, dynamic> json) {
final featuresFromJson = json['features'] as List;
final List<MorphologicalFeature> featuresList = featuresFromJson
.map((feature) => MorphologicalFeature.fromJson(feature))
.toList();
return MorphInfoResponse(
userL1: json['user_l1'],
userL2: json['user_l2'],
features: featuresList,
);
}
Map<String, dynamic> toJson() {
return {
'user_l1': userL1,
'user_l2': userL2,
'features': features.map((feature) => feature.toJson()).toList(),
};
}
MorphologicalFeature? getFeatureByCode(String code) {
return features.firstWhereOrNull(
(feature) => feature.code.toLowerCase() == code.toLowerCase());
}
}
Loading…
Cancel
Save