diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 8a9891467..826963cac 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -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" } \ No newline at end of file diff --git a/lib/pangea/analytics/constants/morph_categories_and_labels.dart b/lib/pangea/analytics/constants/morph_categories_and_labels.dart index 3d896c17e..a63fc75fb 100644 --- a/lib/pangea/analytics/constants/morph_categories_and_labels.dart +++ b/lib/pangea/analytics/constants/morph_categories_and_labels.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; const Map> morphCategoriesAndLabels = { - "Pos": [ + "pos": [ "ADJ", "ADP", "ADV", @@ -24,14 +24,14 @@ const Map> 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> 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> 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> morphCategoriesAndLabels = { "Range", "Dist", ], - "Number": [ + "number": [ "Sing", "Plur", "Dual", @@ -114,19 +114,19 @@ const Map> 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> morphCategoriesAndLabels = { "Rcp", "Int,Rel", ], - "PunctSide": ["Ini", "Fin"], - "PunctType": [ + "punctside": ["Ini", "Fin"], + "puncttype": [ "Brck", "Dash", "Excl", @@ -152,9 +152,9 @@ const Map> 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> 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 getLabelsForMorphCategory(String category) { - for (final feat in morphCategoriesAndLabels.keys) { - if (feat.toLowerCase() == category.toLowerCase()) { - return morphCategoriesAndLabels[feat]!; - } - } - return []; -} +List getLabelsForMorphCategory(String feature) => + morphCategoriesAndLabels[feature.toLowerCase()] ?? []; + +List getMorphCategories() => morphCategoriesAndLabels.keys.toList(); diff --git a/lib/pangea/analytics/controllers/message_analytics_controller.dart b/lib/pangea/analytics/controllers/message_analytics_controller.dart index 9abe97146..38504fd90 100644 --- a/lib/pangea/analytics/controllers/message_analytics_controller.dart +++ b/lib/pangea/analytics/controllers/message_analytics_controller.dart @@ -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'; diff --git a/lib/pangea/analytics/models/construct_identifier.dart b/lib/pangea/analytics/models/construct_identifier.dart new file mode 100644 index 000000000..f9b488f6d --- /dev/null +++ b/lib/pangea/analytics/models/construct_identifier.dart @@ -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 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 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}"; +} diff --git a/lib/pangea/analytics/models/construct_list_model.dart b/lib/pangea/analytics/models/construct_list_model.dart index 3d81f8cfd..431f1b7cc 100644 --- a/lib/pangea/analytics/models/construct_list_model.dart +++ b/lib/pangea/analytics/models/construct_list_model.dart @@ -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. diff --git a/lib/pangea/analytics/models/construct_use_model.dart b/lib/pangea/analytics/models/construct_use_model.dart index bb5b64feb..eb8792200 100644 --- a/lib/pangea/analytics/models/construct_use_model.dart +++ b/lib/pangea/analytics/models/construct_use_model.dart @@ -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 { diff --git a/lib/pangea/analytics/models/constructs_model.dart b/lib/pangea/analytics/models/constructs_model.dart index d2763cdb0..046e0f3f2 100644 --- a/lib/pangea/analytics/models/constructs_model.dart +++ b/lib/pangea/analytics/models/constructs_model.dart @@ -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 { diff --git a/lib/pangea/analytics/utils/get_svg_link.dart b/lib/pangea/analytics/utils/get_svg_link.dart index 30a65e6dd..21e5a15cb 100644 --- a/lib/pangea/analytics/utils/get_svg_link.dart +++ b/lib/pangea/analytics/utils/get_svg_link.dart @@ -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, diff --git a/lib/pangea/analytics/widgets/analytics_summary/learning_progress_indicators.dart b/lib/pangea/analytics/widgets/analytics_summary/learning_progress_indicators.dart index 8570c89aa..9a2548cd8 100644 --- a/lib/pangea/analytics/widgets/analytics_summary/learning_progress_indicators.dart +++ b/lib/pangea/analytics/widgets/analytics_summary/learning_progress_indicators.dart @@ -128,10 +128,7 @@ class LearningProgressIndicatorsState onTap: () { showDialog( context: context, - builder: (c) => MorphAnalyticsPopup( - type: ProgressIndicatorEnum - .morphsUsed.constructType, - ), + builder: (c) => const MorphAnalyticsPopup(), ); }, indicator: ProgressIndicatorEnum.morphsUsed, diff --git a/lib/pangea/analytics/widgets/analytics_summary/morph_analytics_popup/morph_analytics_popup.dart b/lib/pangea/analytics/widgets/analytics_summary/morph_analytics_popup/morph_analytics_popup.dart index 8cfe3fdee..bf068be96 100644 --- a/lib/pangea/analytics/widgets/analytics_summary/morph_analytics_popup/morph_analytics_popup.dart +++ b/lib/pangea/analytics/widgets/analytics_summary/morph_analytics_popup/morph_analytics_popup.dart @@ -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 { - String? selectedCategory; ConstructListModel get _constructsModel => MatrixState.pangeaController.getAnalytics.constructListModel; Map> get _categoriesToUses => - _constructsModel.categoriesToUses(type: widget.type); + _constructsModel.categoriesToUses(type: ConstructTypeEnum.morph); List>> get _sortedEntries { final entries = _categoriesToUses.entries.toList(); @@ -54,92 +48,52 @@ class MorphAnalyticsPopupState extends State { 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('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>> 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( + 0, + (previousValue, element) => + previousValue + element.points, + )}"); + + return MorphFeatureBox(morphIdentifiers: ids); + }, + ), ), ), maxWidth: 600, @@ -148,15 +102,250 @@ class MorphAnalyticsPopupState extends State { } } -class ConstructsTileList extends StatelessWidget { - final List constructs; - const ConstructsTileList(this.constructs, {super.key}); +class MorphFeatureBox extends StatelessWidget { + final List 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 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.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, + }); +} diff --git a/lib/pangea/common/widgets/customized_svg.dart b/lib/pangea/common/widgets/customized_svg.dart index 11042392e..175b4d0e0 100644 --- a/lib/pangea/common/widgets/customized_svg.dart +++ b/lib/pangea/common/widgets/customized_svg.dart @@ -25,10 +25,22 @@ class CustomizedSvg extends StatelessWidget { static final GetStorage _svgStorage = GetStorage('svg_cache'); - Future _fetchSvg() async { - final cachedSvg = _svgStorage.read(svgUrl); - if (cachedSvg != null) { - return cachedSvg; + Future _fetchSvg() async { + final cachedSvgEntry = _svgStorage.read(svgUrl); + if (cachedSvgEntry != null && cachedSvgEntry is Map) { + 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 _getModifiedSvg() async { + Future _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( + return FutureBuilder( 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!); diff --git a/lib/pangea/events/models/pangea_token_model.dart b/lib/pangea/events/models/pangea_token_model.dart index 709cbd1bb..c34bf4637 100644 --- a/lib/pangea/events/models/pangea_token_model.dart +++ b/lib/pangea/events/models/pangea_token_model.dart @@ -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'; diff --git a/lib/pangea/toolbar/enums/activity_type_enum.dart b/lib/pangea/toolbar/enums/activity_type_enum.dart index ed76bfd82..ec051fed8 100644 --- a/lib/pangea/toolbar/enums/activity_type_enum.dart +++ b/lib/pangea/toolbar/enums/activity_type_enum.dart @@ -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, diff --git a/lib/pangea/toolbar/models/practice_activity_model.dart b/lib/pangea/toolbar/models/practice_activity_model.dart index 64704846d..28d88893c 100644 --- a/lib/pangea/toolbar/models/practice_activity_model.dart +++ b/lib/pangea/toolbar/models/practice_activity_model.dart @@ -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 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 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; diff --git a/lib/pangea/toolbar/repo/lemma_meaning_activity_generator.dart b/lib/pangea/toolbar/repo/lemma_meaning_activity_generator.dart index b29c163c7..9e3da8015 100644 --- a/lib/pangea/toolbar/repo/lemma_meaning_activity_generator.dart +++ b/lib/pangea/toolbar/repo/lemma_meaning_activity_generator.dart @@ -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'; diff --git a/lib/pangea/toolbar/repo/morph_activity_generator.dart b/lib/pangea/toolbar/repo/morph_activity_generator.dart index f9a896391..415aa5de1 100644 --- a/lib/pangea/toolbar/repo/morph_activity_generator.dart +++ b/lib/pangea/toolbar/repo/morph_activity_generator.dart @@ -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';