feat: grammar analytics popup redesign (#1670)
This commit is contained in:
parent
44af47a1ee
commit
d5dd66bcc2
16 changed files with 456 additions and 238 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
92
lib/pangea/analytics/models/construct_identifier.dart
Normal file
92
lib/pangea/analytics/models/construct_identifier.dart
Normal 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}";
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue