feat: grammar analytics popup redesign (#1670)

This commit is contained in:
ggurdin 2025-01-31 13:39:10 -05:00 committed by GitHub
parent 44af47a1ee
commit d5dd66bcc2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 456 additions and 238 deletions

View file

@ -4779,5 +4779,6 @@
"mustBeInteger": "Must be an integer e.g. 1, 2, 3, ...",
"noBookmarkedActivities": "No bookmarked activities",
"noLemmasFound": "No lemmas found",
"constructUsePvmDesc": "Produced in voice message"
"constructUsePvmDesc": "Produced in voice message",
"lockedMorphFeature": "Waiting to be unlocked"
}

View file

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
const Map<String, List<String>> morphCategoriesAndLabels = {
"Pos": [
"pos": [
"ADJ",
"ADP",
"ADV",
@ -24,14 +24,14 @@ const Map<String, List<String>> morphCategoriesAndLabels = {
"VERB",
"X",
],
"AdvType": ["Adverbial", "Tim"],
"Aspect": [
"advtype": ["Adverbial", "Tim"],
"aspect": [
"Imp",
"Perf",
"Prog",
"Hab",
],
"Case": [
"case": [
"Nom",
"Acc",
"Dat",
@ -62,18 +62,18 @@ const Map<String, List<String>> morphCategoriesAndLabels = {
"Acc,Nom",
"Pre",
],
"ConjType": ["Coord", "Sub", "Cmp"],
"Definite": ["Def", "Ind", "Cons"],
"Degree": [
"conjtype": ["Coord", "Sub", "Cmp"],
"definite": ["Def", "Ind", "Cons"],
"degree": [
"Pos",
"Cmp",
"Sup",
"Abs",
],
"Evident": ["Fh", "Nfh"],
"Foreign": ["Yes"],
"Gender": ["Masc", "Fem", "Neut", "Com"],
"Mood": [
"evident": ["Fh", "Nfh"],
"foreign": ["Yes"],
"gender": ["Masc", "Fem", "Neut", "Com"],
"mood": [
"Ind",
"Imp",
"Sub",
@ -88,14 +88,14 @@ const Map<String, List<String>> morphCategoriesAndLabels = {
"Qot",
"Int",
],
"NounType": ["Prop", "Comm", "Not_proper"],
"NumForm": [
"nountype": ["Prop", "Comm", "Not_proper"],
"numform": [
"Digit",
"Word",
"Roman",
"Letter",
],
"NumType": [
"numtype": [
"Card",
"Ord",
"Mult",
@ -104,7 +104,7 @@ const Map<String, List<String>> morphCategoriesAndLabels = {
"Range",
"Dist",
],
"Number": [
"number": [
"Sing",
"Plur",
"Dual",
@ -114,19 +114,19 @@ const Map<String, List<String>> morphCategoriesAndLabels = {
"Grpl",
"Inv",
],
"Number[psor]": ["Sing", "Plur", "Dual"],
"Person": [
"number[psor]": ["Sing", "Plur", "Dual"],
"person": [
"0",
"1",
"2",
"3",
"4",
],
"Polarity": ["Pos", "Neg"],
"Polite": ["Infm", "Form", "Elev", "Humb"],
"Poss": ["Yes"],
"PrepCase": ["Npr"],
"PronType": [
"polarity": ["Pos", "Neg"],
"polite": ["Infm", "Form", "Elev", "Humb"],
"poss": ["Yes"],
"prepcase": ["Npr"],
"prontype": [
"Prs",
"Int",
"Rel",
@ -140,8 +140,8 @@ const Map<String, List<String>> morphCategoriesAndLabels = {
"Rcp",
"Int,Rel",
],
"PunctSide": ["Ini", "Fin"],
"PunctType": [
"punctside": ["Ini", "Fin"],
"puncttype": [
"Brck",
"Dash",
"Excl",
@ -152,9 +152,9 @@ const Map<String, List<String>> morphCategoriesAndLabels = {
"Colo",
"Comm",
],
"Reflex": ["Yes"],
"Tense": ["Pres", "Past", "Fut", "Imp", "Pqp", "Aor", "Eps", "Prosp"],
"VerbForm": [
"reflex": ["Yes"],
"tense": ["Pres", "Past", "Fut", "Imp", "Pqp", "Aor", "Eps", "Prosp"],
"verbform": [
"Fin",
"Inf",
"Sup",
@ -165,9 +165,9 @@ const Map<String, List<String>> morphCategoriesAndLabels = {
"Adn",
"Lng",
],
"VerbType": ["Mod", "Caus"],
"Voice": ["Act", "Mid", "Pass", "Antip", "Cau", "Dir", "Inv", "Rcp", "Caus"],
"X": ["X"],
"verbtype": ["Mod", "Caus"],
"voice": ["Act", "Mid", "Pass", "Antip", "Cau", "Dir", "Inv", "Rcp", "Caus"],
"x": ["X"],
};
// TODO Use the icons that Khue is creating
@ -224,11 +224,7 @@ IconData getIconForMorphFeature(String feature) {
}
}
List<String> getLabelsForMorphCategory(String category) {
for (final feat in morphCategoriesAndLabels.keys) {
if (feat.toLowerCase() == category.toLowerCase()) {
return morphCategoriesAndLabels[feat]!;
}
}
return [];
}
List<String> getLabelsForMorphCategory(String feature) =>
morphCategoriesAndLabels[feature.toLowerCase()] ?? [];
List<String> getMorphCategories() => morphCategoriesAndLabels.keys.toList();

View file

@ -3,6 +3,7 @@ import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:fluffychat/pangea/analytics/controllers/get_analytics_controller.dart';
import 'package:fluffychat/pangea/analytics/models/construct_identifier.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';

View file

@ -0,0 +1,92 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:collection/collection.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/pangea/analytics/enums/construct_type_enum.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
class ConstructIdentifier {
final String lemma;
final ConstructTypeEnum type;
final String _category;
ConstructIdentifier({
required this.lemma,
required this.type,
category,
}) : _category = category;
factory ConstructIdentifier.fromJson(Map<String, dynamic> json) {
final categoryEntry = json['cat'] ?? json['categories'];
String? category;
if (categoryEntry != null) {
if (categoryEntry is String) {
category = categoryEntry;
} else if (categoryEntry is List) {
category = categoryEntry.first;
}
}
final type = ConstructTypeEnum.values.firstWhereOrNull(
(e) => e.string == json['type'],
);
if (type == null) {
Sentry.addBreadcrumb(Breadcrumb(message: "type is: ${json['type']}"));
Sentry.addBreadcrumb(Breadcrumb(data: json));
throw Exception("Matching construct type not found");
}
try {
return ConstructIdentifier(
lemma: json['lemma'] as String,
type: type,
category: category ?? "",
);
} catch (e, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: e, s: s, data: json);
rethrow;
}
}
String get category {
if (_category.isEmpty) return "other";
return _category.toLowerCase();
}
Map<String, dynamic> toJson() {
return {
'lemma': lemma,
'type': type.string,
'cat': category,
};
}
// override operator == and hashCode
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ConstructIdentifier &&
other.lemma == lemma &&
other.type == type &&
(category == other.category ||
category.toLowerCase() == "other" ||
other.category.toLowerCase() == "other");
}
@override
int get hashCode {
return lemma.hashCode ^ type.hashCode;
}
String get string {
return "$lemma:${type.string}-$category".toLowerCase();
}
String get partialKey => "$lemma-${type.string}";
}

View file

@ -6,11 +6,11 @@ import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/analytics/constants/analytics_constants.dart';
import 'package:fluffychat/pangea/analytics/enums/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics/models/construct_identifier.dart';
import 'package:fluffychat/pangea/analytics/models/construct_use_model.dart';
import 'package:fluffychat/pangea/analytics/models/constructs_model.dart';
import 'package:fluffychat/pangea/analytics/utils/get_grammar_copy.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/toolbar/models/practice_activity_model.dart';
/// A wrapper around a list of [OneConstructUse]s, used to simplify
/// the process of filtering / sorting / displaying the events.

View file

@ -2,8 +2,8 @@ import 'package:fluffychat/pangea/analytics/constants/analytics_constants.dart';
import 'package:fluffychat/pangea/analytics/enums/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics/enums/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/analytics/enums/lemma_category_enum.dart';
import 'package:fluffychat/pangea/analytics/models/construct_identifier.dart';
import 'package:fluffychat/pangea/analytics/models/constructs_model.dart';
import 'package:fluffychat/pangea/toolbar/models/practice_activity_model.dart';
/// One lemma and a list of construct uses for that lemma
class ConstructUses {

View file

@ -6,8 +6,8 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/analytics/constants/morph_categories_and_labels.dart';
import 'package:fluffychat/pangea/analytics/enums/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/analytics/models/construct_identifier.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/toolbar/models/practice_activity_model.dart';
import '../enums/construct_type_enum.dart';
class ConstructAnalyticsModel {

View file

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
String? getMorphSvgLink({
String getMorphSvgLink({
required String morphFeature,
String? morphTag,
required BuildContext context,

View file

@ -128,10 +128,7 @@ class LearningProgressIndicatorsState
onTap: () {
showDialog<MorphAnalyticsPopup>(
context: context,
builder: (c) => MorphAnalyticsPopup(
type: ProgressIndicatorEnum
.morphsUsed.constructType,
),
builder: (c) => const MorphAnalyticsPopup(),
);
},
indicator: ProgressIndicatorEnum.morphsUsed,

View file

@ -2,35 +2,29 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/analytics/constants/morph_categories_and_labels.dart';
import 'package:fluffychat/pangea/analytics/enums/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics/enums/progress_indicators_enum.dart';
import 'package:fluffychat/pangea/analytics/models/construct_list_model.dart';
import 'package:fluffychat/pangea/analytics/models/construct_use_model.dart';
import 'package:fluffychat/pangea/analytics/widgets/analytics_summary/morph_analytics_popup/morph_analytics_xp_tile.dart';
import 'package:fluffychat/pangea/analytics/utils/get_grammar_copy.dart';
import 'package:fluffychat/pangea/analytics/utils/get_svg_link.dart';
import 'package:fluffychat/pangea/common/widgets/customized_svg.dart';
import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart';
import 'package:fluffychat/utils/color_value.dart';
import 'package:fluffychat/widgets/matrix.dart';
class MorphAnalyticsPopup extends StatefulWidget {
final ConstructTypeEnum type;
final bool showGroups;
class MorphAnalyticsPopup extends StatelessWidget {
const MorphAnalyticsPopup({
required this.type,
this.showGroups = true,
super.key,
});
@override
MorphAnalyticsPopupState createState() => MorphAnalyticsPopupState();
}
class MorphAnalyticsPopupState extends State<MorphAnalyticsPopup> {
String? selectedCategory;
ConstructListModel get _constructsModel =>
MatrixState.pangeaController.getAnalytics.constructListModel;
Map<String, List<ConstructUses>> get _categoriesToUses =>
_constructsModel.categoriesToUses(type: widget.type);
_constructsModel.categoriesToUses(type: ConstructTypeEnum.morph);
List<MapEntry<String, List<ConstructUses>>> get _sortedEntries {
final entries = _categoriesToUses.entries.toList();
@ -54,92 +48,52 @@ class MorphAnalyticsPopupState extends State<MorphAnalyticsPopup> {
return entries;
}
void setSelectedCategory(String? category) => setState(() {
selectedCategory = category;
});
String categoryCopy(category) {
if (category.toLowerCase() == "other") {
return L10n.of(context).other;
}
return widget.type.getDisplayCopy(
category,
context,
) ??
category;
}
@override
Widget build(BuildContext context) {
Widget? dialogContent;
final bool hasNoData =
_constructsModel.constructList(type: widget.type).isEmpty;
final bool hasNoCategories = _categoriesToUses.length == 1 &&
_categoriesToUses.entries.first.key == "Other";
if (selectedCategory != null) {
dialogContent = Column(
children: [
Text(
categoryCopy(selectedCategory),
style: const TextStyle(fontSize: 16),
),
Expanded(
child: ConstructsTileList(
_categoriesToUses[selectedCategory]!
.where((use) => use.points > 0)
.toList(),
),
),
],
);
} else if (hasNoData) {
dialogContent = Center(child: Text(L10n.of(context).noDataFound));
} else if (hasNoCategories || !widget.showGroups) {
dialogContent = ConstructsTileList(
_constructsModel
.constructList(type: widget.type)
.where((uses) => uses.points > 0)
.toList(),
);
} else {
dialogContent = ListView.builder(
// Add a key to the ListView to persist the scroll position
key: const PageStorageKey<String>('categoryList'),
itemCount: _sortedEntries.length,
itemBuilder: (context, index) {
final category = _sortedEntries[index];
return Column(
children: [
ListTile(
title: Text(categoryCopy(category.key)),
trailing: const Icon(Icons.chevron_right_outlined),
onTap: () => setSelectedCategory(category.key),
),
const Divider(height: 1),
],
);
},
);
}
final availableFeatures = getMorphCategories();
final List<MapEntry<String, List<ConstructUses>>> morphUses =
List.from(_sortedEntries);
return FullWidthDialog(
dialogContent: Scaffold(
appBar: AppBar(
title: Text(widget.type.indicator.tooltip(context)),
title: Text(ConstructTypeEnum.morph.indicator.tooltip(context)),
leading: IconButton(
icon: selectedCategory == null
? const Icon(Icons.close)
: const Icon(Icons.chevron_left_outlined),
onPressed: selectedCategory == null
? Navigator.of(context).pop
: () => setSelectedCategory(null),
icon: const Icon(Icons.close),
onPressed: Navigator.of(context).pop,
),
),
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: dialogContent,
child: _constructsModel
.constructList(type: ConstructTypeEnum.morph)
.isEmpty
? Center(child: Text(L10n.of(context).noDataFound))
: ListView.builder(
itemCount: availableFeatures.length,
itemBuilder: (context, index) {
final category =
index < morphUses.length ? morphUses[index] : null;
final ids = category?.value
.map(
(use) => MorphIdentifier(
morphFeature: category.key,
morphTag: use.lemma,
),
)
.toList() ??
[];
debugPrint(
"category: ${category?.key}, points: ${category?.value.fold<int>(
0,
(previousValue, element) =>
previousValue + element.points,
)}");
return MorphFeatureBox(morphIdentifiers: ids);
},
),
),
),
maxWidth: 600,
@ -148,15 +102,250 @@ class MorphAnalyticsPopupState extends State<MorphAnalyticsPopup> {
}
}
class ConstructsTileList extends StatelessWidget {
final List<ConstructUses> constructs;
const ConstructsTileList(this.constructs, {super.key});
class MorphFeatureBox extends StatelessWidget {
final List<MorphIdentifier> morphIdentifiers;
const MorphFeatureBox({
super.key,
required this.morphIdentifiers,
});
String get _morphFeature => morphIdentifiers.first.morphFeature;
String _categoryCopy(
String category,
BuildContext context,
) {
if (category.toLowerCase() == "other") {
return L10n.of(context).other;
}
return ConstructTypeEnum.morph.getDisplayCopy(
category,
context,
) ??
category;
}
Set<String> get _lockedTags {
final availableLabels = getLabelsForMorphCategory(_morphFeature)
.map((tag) => tag.toLowerCase())
.toSet();
final usedLabels =
morphIdentifiers.map((morph) => morph.morphTag.toLowerCase()).toSet();
return availableLabels.difference(usedLabels);
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: constructs.length,
itemBuilder: (context, index) => ConstructUsesXPTile(constructs[index]),
if (morphIdentifiers.isEmpty) {
return Opacity(
opacity: 0.5,
child: Container(
padding: const EdgeInsets.all(24.0),
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0),
color: Theme.of(context).disabledColor,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.lock, size: 24.0),
const SizedBox(width: 24.0),
Text(
L10n.of(context).lockedMorphFeature,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
final allMorphIdentifiers = List.from(morphIdentifiers);
allMorphIdentifiers.addAll(
_lockedTags.map(
(tag) => MorphIdentifier(
morphFeature: _morphFeature,
morphTag: tag,
isLocked: true,
),
),
);
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.all(16.0),
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0),
border: Border.all(color: AppConfig.gold, width: 2),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 16.0,
children: [
SizedBox(
height: 30.0,
width: 30.0,
child: CustomizedSvg(
svgUrl: getMorphSvgLink(
morphFeature: _morphFeature,
context: context,
),
colorReplacements: theme.brightness == Brightness.dark
? {
"white": theme.cardColor.hexValue.toString(),
"black": "white",
}
: {},
errorIcon: Icon(getIconForMorphFeature(_morphFeature)),
),
),
Text(
_categoryCopy(_morphFeature, context),
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16.0),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Wrap(
alignment: WrapAlignment.center,
spacing: 16.0,
runSpacing: 16.0,
children: [
for (final morph in allMorphIdentifiers)
MorphTagChip(morphIdentifier: morph),
],
),
),
],
),
],
),
);
}
}
class MorphTagChip extends StatelessWidget {
final MorphIdentifier morphIdentifier;
const MorphTagChip({
super.key,
required this.morphIdentifier,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
if (morphIdentifier.isLocked) {
return Container(
padding: const EdgeInsets.symmetric(
vertical: 4.0,
horizontal: 24.0,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32.0),
color: theme.disabledColor,
),
child: SizedBox(
width: 28.0,
height: 28.0,
child: Icon(
Icons.lock,
color: theme.brightness == Brightness.dark
? Colors.black
: Colors.white,
),
),
);
}
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32.0),
gradient: LinearGradient(
begin: Alignment.topLeft,
colors: <Color>[
Color.alphaBlend(
theme.brightness == Brightness.dark
? Colors.black.withAlpha(220)
: Colors.white.withAlpha(220),
AppConfig.gold,
),
AppConfig.gold,
],
),
),
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: CustomizedSvg(
svgUrl: getMorphSvgLink(
morphFeature: morphIdentifier.morphFeature,
morphTag: morphIdentifier.morphTag,
context: context,
),
colorReplacements: theme.brightness == Brightness.dark
? {
"white": theme.cardColor.hexValue.toString(),
"black": "white",
}
: {},
errorIcon: Icon(
getIconForMorphFeature(morphIdentifier.morphFeature),
),
),
),
Text(
getGrammarCopy(
category: morphIdentifier.morphFeature,
lemma: morphIdentifier.morphTag,
context: context,
) ??
morphIdentifier.morphTag,
style: TextStyle(
fontWeight: FontWeight.bold,
color: theme.brightness == Brightness.dark
? Colors.white
: Colors.black,
),
),
],
),
);
}
}
class MorphIdentifier {
final String morphFeature;
final String morphTag;
final bool isLocked;
const MorphIdentifier({
required this.morphFeature,
required this.morphTag,
this.isLocked = false,
});
}

View file

@ -25,10 +25,22 @@ class CustomizedSvg extends StatelessWidget {
static final GetStorage _svgStorage = GetStorage('svg_cache');
Future<String> _fetchSvg() async {
final cachedSvg = _svgStorage.read(svgUrl);
if (cachedSvg != null) {
return cachedSvg;
Future<String?> _fetchSvg() async {
final cachedSvgEntry = _svgStorage.read(svgUrl);
if (cachedSvgEntry != null && cachedSvgEntry is Map<String, dynamic>) {
final cachedSvg = cachedSvgEntry['svg'] as String?;
final timestamp = cachedSvgEntry['timestamp'] as int?;
if (cachedSvg != null) {
return cachedSvg;
}
// if timestamp is younger than 1 day, return null
if (timestamp != null &&
DateTime.now()
.difference(DateTime.fromMillisecondsSinceEpoch(timestamp))
.inDays <
1) {
return null;
}
}
final response = await http.get(Uri.parse(svgUrl));
@ -40,35 +52,46 @@ class CustomizedSvg extends StatelessWidget {
"svgUrl": svgUrl,
},
);
await _svgStorage.write(
svgUrl,
{'timestamp': DateTime.now().millisecondsSinceEpoch},
);
throw e;
}
final String svgContent = response.body;
await _svgStorage.write(svgUrl, svgContent);
await _svgStorage.write(svgUrl, {
'svg': svgContent,
'timestamp': DateTime.now().millisecondsSinceEpoch,
});
return svgContent;
}
Future<String> _getModifiedSvg() async {
Future<String?> _getModifiedSvg() async {
final svgContent = await _fetchSvg();
String modifiedSvg = svgContent;
String? modifiedSvg = svgContent;
if (modifiedSvg == null) {
return null;
}
// find the white and replace with black
// or find black and replace with white
modifiedSvg = modifiedSvg.replaceAll("fill=\"none\"", '');
for (final entry in colorReplacements.entries) {
modifiedSvg = modifiedSvg.replaceAll(entry.key, entry.value);
modifiedSvg = modifiedSvg!.replaceAll(entry.key, entry.value);
}
return modifiedSvg;
}
@override
Widget build(BuildContext context) {
return FutureBuilder<String>(
return FutureBuilder<String?>(
future: _getModifiedSvg(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
} else if (snapshot.hasError) {
} else if (snapshot.hasError || snapshot.data == null) {
return errorIcon;
} else if (snapshot.hasData) {
return SvgPicture.string(snapshot.data!);

View file

@ -10,6 +10,7 @@ import 'package:fluffychat/pangea/analytics/enums/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics/enums/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/analytics/enums/lemma_category_enum.dart';
import 'package:fluffychat/pangea/analytics/extensions/client_analytics_extension.dart';
import 'package:fluffychat/pangea/analytics/models/construct_identifier.dart';
import 'package:fluffychat/pangea/analytics/models/construct_use_model.dart';
import 'package:fluffychat/pangea/analytics/models/constructs_model.dart';
import 'package:fluffychat/pangea/analytics/repo/lemma_info_repo.dart';
@ -19,7 +20,6 @@ import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
import 'package:fluffychat/pangea/toolbar/models/practice_activity_model.dart';
import 'package:fluffychat/pangea/toolbar/repo/lemma_activity_generator.dart';
import 'package:fluffychat/pangea/toolbar/repo/lemma_meaning_activity_generator.dart';
import 'package:fluffychat/widgets/matrix.dart';

View file

@ -4,7 +4,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:fluffychat/pangea/analytics/enums/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics/enums/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/toolbar/models/practice_activity_model.dart';
import 'package:fluffychat/pangea/analytics/models/construct_identifier.dart';
enum ActivityTypeEnum {
wordMeaning,

View file

@ -9,95 +9,12 @@ import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/pangea/analytics/enums/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics/enums/morph_categories_enum.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/analytics/models/construct_identifier.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/toolbar/enums/activity_display_instructions_enum.dart';
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
import 'package:fluffychat/pangea/toolbar/models/multiple_choice_activity_model.dart';
class ConstructIdentifier {
final String lemma;
final ConstructTypeEnum type;
final String _category;
ConstructIdentifier({
required this.lemma,
required this.type,
category,
}) : _category = category;
factory ConstructIdentifier.fromJson(Map<String, dynamic> json) {
final categoryEntry = json['cat'] ?? json['categories'];
String? category;
if (categoryEntry != null) {
if (categoryEntry is String) {
category = categoryEntry;
} else if (categoryEntry is List) {
category = categoryEntry.first;
}
}
final type = ConstructTypeEnum.values.firstWhereOrNull(
(e) => e.string == json['type'],
);
if (type == null) {
Sentry.addBreadcrumb(Breadcrumb(message: "type is: ${json['type']}"));
Sentry.addBreadcrumb(Breadcrumb(data: json));
throw Exception("Matching construct type not found");
}
try {
return ConstructIdentifier(
lemma: json['lemma'] as String,
type: type,
category: category ?? "",
);
} catch (e, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: e, s: s, data: json);
rethrow;
}
}
String get category {
if (_category.isEmpty) return "other";
return _category.toLowerCase();
}
Map<String, dynamic> toJson() {
return {
'lemma': lemma,
'type': type.string,
'cat': category,
};
}
// override operator == and hashCode
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ConstructIdentifier &&
other.lemma == lemma &&
other.type == type &&
(category == other.category ||
category.toLowerCase() == "other" ||
other.category.toLowerCase() == "other");
}
@override
int get hashCode {
return lemma.hashCode ^ type.hashCode;
}
String get string {
return "$lemma:${type.string}-$category".toLowerCase();
}
String get partialKey => "$lemma-${type.string}";
}
class CandidateMessage {
final String msgId;
final String roomId;

View file

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics/enums/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics/models/construct_identifier.dart';
import 'package:fluffychat/pangea/analytics/models/construct_use_model.dart';
import 'package:fluffychat/pangea/analytics/repo/lemma_info_repo.dart';
import 'package:fluffychat/pangea/analytics/repo/lemma_info_request.dart';

View file

@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
import 'package:fluffychat/pangea/analytics/enums/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics/enums/morph_categories_enum.dart';
import 'package:fluffychat/pangea/analytics/models/construct_identifier.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';