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>
This commit is contained in:
parent
8988cce68a
commit
a71f519700
11 changed files with 784 additions and 288 deletions
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
174
lib/pangea/analytics_details_popup/morph_details_view.dart
Normal file
174
lib/pangea/analytics_details_popup/morph_details_view.dart
Normal file
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Wrap(
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).meaningSectionHeader,
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text:
|
||||
" ${formString ?? L10n.of(context).formsNotFound}",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
const CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
child: Divider(
|
||||
height: 3,
|
||||
color: textColor.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text:
|
||||
" ${_formString ?? L10n.of(context).formsNotFound}",
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
"${construct.points} XP",
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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";
|
||||
|
|
|
|||
99
lib/pangea/morphs/morph_meaning/morph_info_repo.dart
Normal file
99
lib/pangea/morphs/morph_meaning/morph_info_repo.dart
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
30
lib/pangea/morphs/morph_meaning/morph_info_request.dart
Normal file
30
lib/pangea/morphs/morph_meaning/morph_info_request.dart
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
112
lib/pangea/morphs/morph_meaning/morph_info_response.dart
Normal file
112
lib/pangea/morphs/morph_meaning/morph_info_response.dart
Normal file
|
|
@ -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…
Add table
Reference in a new issue