Vocab v2 edits (#1525)
* Emoji as getter, add LearningSkillsEnum * Remove hard-coding for font styles * Remove excess state saving * Remove type+point parameters from definition popup * Fix emoji null check notation * Edit dot widget size for android * Further reduce state saving in definition popup * Removed more hardcoding * fix: UI updates to vocab analytics popup --------- Co-authored-by: ggurdin <ggurdin@gmail.com>
This commit is contained in:
parent
b1b96a9cfd
commit
e2b991b36b
8 changed files with 694 additions and 750 deletions
|
|
@ -4664,9 +4664,9 @@
|
|||
"meaningSectionHeader": "Meaning:",
|
||||
"formSectionHeader": "Forms used in chats:",
|
||||
"noEmojiSelectedTooltip": "No emoji selected",
|
||||
"writingExercisesTooltip": "Writing exercises",
|
||||
"listeningExercisesTooltip": "Listening exercises",
|
||||
"readingExercisesTooltip": "Reading exercises",
|
||||
"writingExercisesTooltip": "Writing activities",
|
||||
"listeningExercisesTooltip": "Listening activities",
|
||||
"readingExercisesTooltip": "Reading activities",
|
||||
"meaningNotFound": "Meaning could not be found.",
|
||||
"formsNotFound": "Forms could not be found.",
|
||||
"chooseBestDefinition": "What does this word mean?",
|
||||
|
|
@ -4770,5 +4770,6 @@
|
|||
"activityPlannerOverviewInstructionsBody": "Choose a topic, mode, learning objective and generate an activity for the chat!",
|
||||
"completeActivitiesToUnlock": "Complete the highlighted word activities to unlock",
|
||||
"myBookmarkedActivities": "My Bookmarked Activities",
|
||||
"noBookmarkedActivities": "No bookmarked activities"
|
||||
"noBookmarkedActivities": "No bookmarked activities",
|
||||
"noLemmasFound": "No lemmas found"
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics/enums/analytics_summary_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics/enums/learning_skills_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
|
||||
enum ConstructUseTypeEnum {
|
||||
|
|
@ -247,6 +248,41 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
}
|
||||
}
|
||||
|
||||
/// Categorize construct use types as writing, reading, speaking, hearing, and other
|
||||
LearningSkillsEnum get skillsEnumType {
|
||||
switch (this) {
|
||||
case ConstructUseTypeEnum.wa:
|
||||
case ConstructUseTypeEnum.ga:
|
||||
case ConstructUseTypeEnum.unk:
|
||||
case ConstructUseTypeEnum.corIt:
|
||||
case ConstructUseTypeEnum.ignIt:
|
||||
case ConstructUseTypeEnum.incIt:
|
||||
case ConstructUseTypeEnum.corIGC:
|
||||
case ConstructUseTypeEnum.ignIGC:
|
||||
case ConstructUseTypeEnum.incIGC:
|
||||
case ConstructUseTypeEnum.corL:
|
||||
case ConstructUseTypeEnum.ignL:
|
||||
case ConstructUseTypeEnum.incL:
|
||||
case ConstructUseTypeEnum.corM:
|
||||
case ConstructUseTypeEnum.ignM:
|
||||
case ConstructUseTypeEnum.incM:
|
||||
return LearningSkillsEnum.writing;
|
||||
case ConstructUseTypeEnum.corWL:
|
||||
case ConstructUseTypeEnum.ignWL:
|
||||
case ConstructUseTypeEnum.incWL:
|
||||
case ConstructUseTypeEnum.corHWL:
|
||||
case ConstructUseTypeEnum.ignHWL:
|
||||
case ConstructUseTypeEnum.incHWL:
|
||||
return LearningSkillsEnum.hearing;
|
||||
case ConstructUseTypeEnum.corPA:
|
||||
case ConstructUseTypeEnum.ignPA:
|
||||
case ConstructUseTypeEnum.incPA:
|
||||
return LearningSkillsEnum.reading;
|
||||
default:
|
||||
return LearningSkillsEnum.other;
|
||||
}
|
||||
}
|
||||
|
||||
AnalyticsSummaryEnum? get summaryEnumType {
|
||||
switch (this) {
|
||||
case ConstructUseTypeEnum.wa:
|
||||
|
|
|
|||
7
lib/pangea/analytics/enums/learning_skills_enum.dart
Normal file
7
lib/pangea/analytics/enums/learning_skills_enum.dart
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
enum LearningSkillsEnum {
|
||||
writing,
|
||||
reading,
|
||||
speaking,
|
||||
hearing,
|
||||
other,
|
||||
}
|
||||
|
|
@ -211,6 +211,12 @@ class ConstructListModel {
|
|||
}
|
||||
}
|
||||
|
||||
List<ConstructUses> getConstructUsesByLemma(String lemma) {
|
||||
return _constructList.where((constructUse) {
|
||||
return constructUse.lemma == lemma;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
List<ConstructUses> constructList({ConstructTypeEnum? type}) => _constructList
|
||||
.where(
|
||||
(constructUse) => type == null || constructUse.constructType == type,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
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/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/practice_activity_model.dart';
|
||||
|
||||
|
|
@ -64,4 +66,14 @@ class ConstructUses {
|
|||
};
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Get the lemma category, based on points
|
||||
LemmaCategoryEnum get lemmaCategory {
|
||||
if (points < AnalyticsConstants.xpForGreens) {
|
||||
return LemmaCategoryEnum.seeds;
|
||||
} else if (points >= AnalyticsConstants.xpForFlower) {
|
||||
return LemmaCategoryEnum.flowers;
|
||||
}
|
||||
return LemmaCategoryEnum.greens;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
|
|
@ -40,126 +41,208 @@ class VocabAnalyticsPopupState extends State<VocabAnalyticsPopup> {
|
|||
return entries;
|
||||
}
|
||||
|
||||
/// Produces list of chips with lemma content,
|
||||
/// and assigns them to flowers, greens, and seeds tiles
|
||||
Widget get dialogContent {
|
||||
if (_constructsModel.constructList(type: ConstructTypeEnum.vocab).isEmpty) {
|
||||
ConstructUses? _selectedConstruct;
|
||||
|
||||
void _setSelectedConstruct(ConstructUses? construct) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_selectedConstruct = construct;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FullWidthDialog(
|
||||
dialogContent: _selectedConstruct == null
|
||||
? LemmaListDialogContent(
|
||||
lemmas: _sortedEntries,
|
||||
onTap: _setSelectedConstruct,
|
||||
)
|
||||
: VocabDefinitionPopup(
|
||||
construct: _selectedConstruct!,
|
||||
onClose: () => _setSelectedConstruct(null),
|
||||
),
|
||||
maxWidth: 600,
|
||||
maxHeight: 800,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VocabChip {
|
||||
final ConstructUses construct;
|
||||
final String? displayText;
|
||||
|
||||
VocabChip({
|
||||
required this.construct,
|
||||
this.displayText,
|
||||
});
|
||||
}
|
||||
|
||||
class LemmaListSection extends StatelessWidget {
|
||||
final LemmaCategoryEnum type;
|
||||
final List<VocabChip> lemmas;
|
||||
final Function(ConstructUses) onTap;
|
||||
|
||||
const LemmaListSection({
|
||||
super.key,
|
||||
required this.type,
|
||||
required this.lemmas,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
margin: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 16.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
color: type.color,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CustomizedSvg(
|
||||
svgUrl: type.svgURL,
|
||||
colorReplacements: const {},
|
||||
errorIcon: Text(type.emoji),
|
||||
),
|
||||
Text(
|
||||
" ${type.xpString} XP",
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.bodyLarge?.fontSize,
|
||||
color: Theme.of(context).colorScheme.onPrimaryFixed,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: lemmas.isEmpty
|
||||
? Text(
|
||||
L10n.of(context).noLemmasFound,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimaryFixed,
|
||||
),
|
||||
)
|
||||
: Wrap(
|
||||
spacing: 0,
|
||||
runSpacing: 0,
|
||||
children: lemmas.mapIndexed((index, lemma) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () => onTap(lemma.construct),
|
||||
child: Text(
|
||||
"${lemma.displayText ?? lemma.construct.lemma}${index < lemmas.length - 1 ? ', ' : ''}",
|
||||
style: TextStyle(
|
||||
color: Colors.transparent,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryFixed,
|
||||
offset: const Offset(0, -3),
|
||||
),
|
||||
],
|
||||
decoration: TextDecoration.underline,
|
||||
decorationStyle: TextDecorationStyle.dashed,
|
||||
decorationColor:
|
||||
Theme.of(context).colorScheme.onPrimaryFixed,
|
||||
decorationThickness: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LemmaListDialogContent extends StatelessWidget {
|
||||
final List<ConstructUses> lemmas;
|
||||
final Function(ConstructUses) onTap;
|
||||
|
||||
const LemmaListDialogContent({
|
||||
super.key,
|
||||
required this.lemmas,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (lemmas.isEmpty) {
|
||||
return Center(child: Text(L10n.of(context).noDataFound));
|
||||
}
|
||||
final sortedEntries = _sortedEntries;
|
||||
|
||||
// Get lists of lemmas by category
|
||||
final List<Widget> flowerLemmas = [];
|
||||
final List<Widget> greenLemmas = [];
|
||||
final List<Widget> seedLemmas = [];
|
||||
for (int i = 0; i < sortedEntries.length; i++) {
|
||||
final construct = sortedEntries[i];
|
||||
final List<VocabChip> flowerLemmas = [];
|
||||
final List<VocabChip> greenLemmas = [];
|
||||
final List<VocabChip> seedLemmas = [];
|
||||
|
||||
for (int i = 0; i < lemmas.length; i++) {
|
||||
final construct = lemmas[i];
|
||||
if (construct.lemma.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final int points = construct.points;
|
||||
String? displayText;
|
||||
|
||||
// Check if previous or next entry has same lemma as this entry
|
||||
if ((i > 0 && sortedEntries[i - 1].lemma.equals(construct.lemma)) ||
|
||||
((i < sortedEntries.length - 1 &&
|
||||
sortedEntries[i + 1].lemma.equals(construct.lemma)))) {
|
||||
if ((i > 0 && lemmas[i - 1].lemma.equals(construct.lemma)) ||
|
||||
((i < lemmas.length - 1 &&
|
||||
lemmas[i + 1].lemma.equals(construct.lemma)))) {
|
||||
final String pos = getGrammarCopy(
|
||||
category: "pos",
|
||||
lemma: construct.category,
|
||||
context: context,
|
||||
) ??
|
||||
construct.category;
|
||||
displayText = "${sortedEntries[i].lemma} (${pos.toLowerCase()})";
|
||||
displayText = "${lemmas[i].lemma} (${pos.toLowerCase()})";
|
||||
}
|
||||
|
||||
// Add VocabChip for lemma to relevant widget list, followed by comma
|
||||
final lemma = VocabChip(
|
||||
construct: construct,
|
||||
displayText: displayText,
|
||||
);
|
||||
|
||||
if (points < AnalyticsConstants.xpForGreens) {
|
||||
seedLemmas.add(
|
||||
VocabChip(
|
||||
construct: construct,
|
||||
displayText: displayText,
|
||||
onTap: () {
|
||||
showDialog<VocabDefinitionPopup>(
|
||||
context: context,
|
||||
builder: (c) => VocabDefinitionPopup(
|
||||
construct: construct,
|
||||
type: LemmaCategoryEnum.seeds,
|
||||
points: points,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
seedLemmas.add(
|
||||
const Text(
|
||||
", ",
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
);
|
||||
seedLemmas.add(lemma);
|
||||
} else if (points >= AnalyticsConstants.xpForFlower) {
|
||||
flowerLemmas.add(
|
||||
VocabChip(
|
||||
construct: construct,
|
||||
displayText: displayText,
|
||||
onTap: () {
|
||||
showDialog<VocabDefinitionPopup>(
|
||||
context: context,
|
||||
builder: (c) => VocabDefinitionPopup(
|
||||
construct: construct,
|
||||
type: LemmaCategoryEnum.flowers,
|
||||
points: points,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
flowerLemmas.add(
|
||||
const Text(
|
||||
", ",
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
);
|
||||
flowerLemmas.add(lemma);
|
||||
} else {
|
||||
greenLemmas.add(
|
||||
VocabChip(
|
||||
construct: construct,
|
||||
displayText: displayText,
|
||||
onTap: () {
|
||||
showDialog<VocabDefinitionPopup>(
|
||||
context: context,
|
||||
builder: (c) => VocabDefinitionPopup(
|
||||
construct: construct,
|
||||
type: LemmaCategoryEnum.greens,
|
||||
points: points,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
greenLemmas.add(
|
||||
const Text(
|
||||
", ",
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
);
|
||||
greenLemmas.add(lemma);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass sorted lemmas to background tile widgets
|
||||
final Widget flowers =
|
||||
dialogWidget(LemmaCategoryEnum.flowers, flowerLemmas);
|
||||
final Widget greens = dialogWidget(LemmaCategoryEnum.greens, greenLemmas);
|
||||
final Widget seeds = dialogWidget(LemmaCategoryEnum.seeds, seedLemmas);
|
||||
final Widget flowers = LemmaListSection(
|
||||
type: LemmaCategoryEnum.flowers,
|
||||
lemmas: flowerLemmas,
|
||||
onTap: onTap,
|
||||
);
|
||||
|
||||
final Widget greens = LemmaListSection(
|
||||
type: LemmaCategoryEnum.greens,
|
||||
lemmas: greenLemmas,
|
||||
onTap: onTap,
|
||||
);
|
||||
|
||||
final Widget seeds = LemmaListSection(
|
||||
type: LemmaCategoryEnum.seeds,
|
||||
lemmas: seedLemmas,
|
||||
onTap: onTap,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
|
|
@ -178,120 +261,4 @@ class VocabAnalyticsPopupState extends State<VocabAnalyticsPopup> {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Tile that contains flowers, greens, or seeds chips
|
||||
Widget dialogWidget(LemmaCategoryEnum type, List<Widget> lemmaList) {
|
||||
// Remove extraneous commas from lemmaList
|
||||
if (lemmaList.isNotEmpty) {
|
||||
lemmaList.removeLast();
|
||||
} else {
|
||||
lemmaList.add(
|
||||
const Text(
|
||||
"No lemmas",
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 10, 20, 10),
|
||||
child: Material(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(AppConfig.borderRadius)),
|
||||
color: type.color,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(
|
||||
10,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CustomizedSvg(
|
||||
svgUrl: type.svgURL,
|
||||
colorReplacements: const {},
|
||||
errorIcon: Text(type.emoji),
|
||||
),
|
||||
Text(
|
||||
" ${type.xpString} XP",
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
Wrap(
|
||||
spacing: 0,
|
||||
runSpacing: 0,
|
||||
children: lemmaList,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FullWidthDialog(
|
||||
dialogContent: dialogContent,
|
||||
maxWidth: 600,
|
||||
maxHeight: 800,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple chip with the text of the lemma
|
||||
// TODO: highlights on hover
|
||||
// callback on click
|
||||
// has some padding to separate from other chips
|
||||
// otherwise, is very visually simple with transparent border/background/etc
|
||||
class VocabChip extends StatelessWidget {
|
||||
final ConstructUses construct;
|
||||
final String? displayText;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const VocabChip({
|
||||
super.key,
|
||||
required this.construct,
|
||||
required this.onTap,
|
||||
this.displayText,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Text(
|
||||
displayText ?? construct.lemma,
|
||||
style: const TextStyle(
|
||||
// Workaround to add space between text and underline
|
||||
color: Colors.transparent,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: Colors.black,
|
||||
offset: Offset(0, -3),
|
||||
),
|
||||
],
|
||||
decoration: TextDecoration.underline,
|
||||
decorationStyle: TextDecorationStyle.dashed,
|
||||
decorationColor: Colors.black,
|
||||
decorationThickness: 1,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -8,13 +8,15 @@ import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
|
|||
class WordAudioButton extends StatefulWidget {
|
||||
final String text;
|
||||
final TtsController ttsController;
|
||||
final String eventID;
|
||||
final String? eventID;
|
||||
final double size;
|
||||
|
||||
const WordAudioButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.ttsController,
|
||||
required this.eventID,
|
||||
this.eventID,
|
||||
this.size = 24,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -31,14 +33,8 @@ class WordAudioButtonState extends State<WordAudioButton> {
|
|||
isSelected: _isPlaying,
|
||||
selectedIcon: const Icon(Icons.pause_outlined),
|
||||
color: _isPlaying ? Colors.white : null,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
_isPlaying
|
||||
? Theme.of(context).colorScheme.secondary
|
||||
: Theme.of(context).colorScheme.primaryContainer,
|
||||
),
|
||||
),
|
||||
tooltip: _isPlaying ? L10n.of(context).stop : L10n.of(context).playAudio,
|
||||
iconSize: widget.size,
|
||||
onPressed: () async {
|
||||
if (_isPlaying) {
|
||||
await widget.ttsController.stop();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue