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>
pull/1593/head
Kelrap 9 months ago committed by GitHub
parent b1b96a9cfd
commit e2b991b36b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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:

@ -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,
),
),
);
}
}

@ -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…
Cancel
Save