refactor: remove ability to directly edit lemma defintions and morph assignments (#4347)

This commit is contained in:
ggurdin 2025-10-13 11:08:44 -04:00 committed by GitHub
parent 97984f85eb
commit 181c4a369b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 48 additions and 818 deletions

View file

@ -50,8 +50,6 @@ class PApiUrls {
static String lemmaDictionary =
"${PApiUrls._choreoEndpoint}/lemma_definition";
static String lemmaDictionaryEdit =
"${PApiUrls._choreoEndpoint}/lemma_definition/edit";
static String morphDictionary = "${PApiUrls._choreoEndpoint}/morph_meaning";
// static String activityPlan = "${PApiUrls._choreoEndpoint}/activity_plan";

View file

@ -1,40 +0,0 @@
class LemmaEditRequest {
String lemma;
String partOfSpeech;
String lemmaLang;
String userL1;
String? newMeaning;
List<String>? newEmojis;
LemmaEditRequest({
required this.lemma,
required this.partOfSpeech,
required this.lemmaLang,
required this.userL1,
this.newMeaning,
this.newEmojis,
});
Map<String, dynamic> toJson() {
return {
"lemma": lemma,
"part_of_speech": partOfSpeech,
"lemma_lang": lemmaLang,
"user_l1": userL1,
"new_meaning": newMeaning,
"new_emojis": newEmojis,
};
}
factory LemmaEditRequest.fromJson(Map<String, dynamic> json) {
return LemmaEditRequest(
lemma: json["lemma"],
partOfSpeech: json["part_of_speech"],
lemmaLang: json["lemma_lang"],
userL1: json["user_l1"],
newMeaning: json["new_meaning"],
newEmojis: List<String>.from(json["new_emojis"] ?? []),
);
}
}

View file

@ -6,7 +6,6 @@ import 'package:http/http.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/network/requests.dart';
import 'package:fluffychat/pangea/common/network/urls.dart';
import 'package:fluffychat/pangea/lemmas/lemma_edit_request.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_request.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -57,35 +56,4 @@ class LemmaInfoRepo {
set(request, response);
return response;
}
static Future<void> edit(LemmaEditRequest request) async {
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: MatrixState.pangeaController.userController.accessToken,
);
final resp = await req.post(
url: PApiUrls.lemmaDictionaryEdit,
body: request.toJson(),
);
if (resp.statusCode != 200) {
throw Exception(
'Failed to edit lemma: ${resp.statusCode} ${resp.body}',
);
}
final decodedBody = jsonDecode(utf8.decode(resp.bodyBytes));
final response = LemmaInfoResponse.fromJson(decodedBody);
set(
LemmaInfoRequest(
lemma: request.lemma,
partOfSpeech: request.partOfSpeech,
lemmaLang: request.lemmaLang,
userL1: request.userL1,
),
response,
);
}
}

View file

@ -1,234 +0,0 @@
import 'package:fluffychat/pangea/morphs/morph_models.dart';
final MorphFeaturesAndTags defaultUDMapping = MorphFeaturesAndTags.fromJson({
"language_code": "default",
"features": [
{
"feature": "pos",
"tag": [
"ADJ",
"ADP",
"ADV",
"AFFIX",
"AUX",
"CCONJ",
"DET",
"INTJ",
"NOUN",
"NUM",
"PART",
"PRON",
"PROPN",
"PUNCT",
"SCONJ",
"SPACE",
"SYM",
"VERB",
"X",
],
},
{
"feature": "advtype",
"tag": ["Adverbial", "Tim"],
},
{
"feature": "aspect",
"tag": ["Imp", "Perf", "Prog", "Hab"],
},
{
"feature": "case",
"tag": [
"Nom",
"Acc",
"Dat",
"Gen",
"Voc",
"Abl",
"Loc",
"All",
"Ins",
"Ess",
"Tra",
"Com",
"Par",
"Adv",
"Ref",
"Rel",
"Equ",
"Dis",
"Abs",
"Erg",
"Cau",
"Ben",
"Sub",
"Sup",
"Tem",
"Obl",
"Acc,Dat",
"Acc,Nom",
"Pre",
],
},
{
"feature": "conjtype",
"tag": ["Coord", "Sub", "Cmp"],
},
{
"feature": "definite",
"tag": ["Def", "Ind", "Cons"],
},
{
"feature": "degree",
"tag": ["Pos", "Cmp", "Sup", "Abs"],
},
{
"feature": "evident",
"tag": ["Fh", "Nfh"],
},
{
"feature": "foreign",
"tag": ["Yes"],
},
{
"feature": "gender",
"tag": ["Masc", "Fem", "Neut", "Com"],
},
{
"feature": "mood",
"tag": [
"Ind",
"Imp",
"Sub",
"Cnd",
"Opt",
"Jus",
"Adm",
"Des",
"Nec",
"Pot",
"Prp",
"Qot",
"Int",
],
},
{
"feature": "nountype",
"tag": ["Prop", "Comm", "Not_proper"],
},
{
"feature": "numform",
"tag": ["Digit", "Word", "Roman", "Letter"],
},
{
"feature": "numtype",
"tag": ["Card", "Ord", "Mult", "Frac", "Sets", "Range", "Dist"],
},
{
"feature": "number",
"tag": ["Sing", "Plur", "Dual", "Tri", "Pauc", "Grpa", "Grpl", "Inv"],
},
{
"feature": "number[psor]",
"tag": ["Sing", "Plur", "Dual"],
},
{
"feature": "person",
"tag": ["0", "1", "2", "3", "4"],
},
{
"feature": "polarity",
"tag": ["Pos", "Neg"],
},
{
"feature": "polite",
"tag": ["Infm", "Form", "Elev", "Humb"],
},
{
"feature": "poss",
"tag": ["Yes"],
},
{
"feature": "prepcase",
"tag": ["Npr"],
},
{
"feature": "prontype",
"tag": [
"Prs",
"Int",
"Rel",
"Dem",
"Tot",
"Neg",
"Art",
"Emp",
"Exc",
"Ind",
"Rcp",
"Int,Rel",
],
},
{
"feature": "punctside",
"tag": ["Ini", "Fin"],
},
{
"feature": "puncttype",
"tag": [
"Brck",
"Dash",
"Excl",
"Peri",
"Qest",
"Quot",
"Semi",
"Colo",
"Comm",
],
},
{
"feature": "reflex",
"tag": ["Yes"],
},
{
"feature": "tense",
"tag": ["Pres", "Past", "Fut", "Imp", "Pqp", "Aor", "Eps", "Prosp"],
},
{
"feature": "verbform",
"tag": [
"Fin",
"Inf",
"Sup",
"Part",
"Conv",
"Vnoun",
"Ger",
"Adn",
"Lng",
],
},
{
"feature": "verbtype",
"tag": ["Mod", "Caus"],
},
{
"feature": "voice",
"tag": [
"Act",
"Mid",
"Pass",
"Antip",
"Cau",
"Dir",
"Inv",
"Rcp",
"Caus",
],
},
{
"feature": "x",
"tag": ["X"],
}
],
});

View file

@ -1,258 +0,0 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.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/events/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/morphs/default_morph_mapping.dart';
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/morphs/morph_repo.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
class EditMorphWidget extends StatefulWidget {
final PangeaToken token;
final PangeaMessageEvent pangeaMessageEvent;
final MorphFeaturesEnum morphFeature;
final VoidCallback onClose;
const EditMorphWidget({
required this.token,
required this.pangeaMessageEvent,
required this.morphFeature,
required this.onClose,
super.key,
});
@override
State<EditMorphWidget> createState() => EditMorphWidgetState();
}
class EditMorphWidgetState extends State<EditMorphWidget> {
List<String>? _availableMorphTags;
String? _selectedMorphTag;
@override
void initState() {
super.initState();
_setAvailableMorphs(widget.morphFeature);
_selectedMorphTag = _assignedMorphTag;
}
@override
void didUpdateWidget(covariant EditMorphWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.morphFeature != widget.morphFeature) {
_setAvailableMorphs(widget.morphFeature);
}
}
String? get _assignedMorphTag => widget.token.morph[widget.morphFeature];
Future<void> _setAvailableMorphs(MorphFeaturesEnum feature) async {
try {
setState(() => _availableMorphTags = null);
final resp = await MorphsRepo.get();
_availableMorphTags = resp.getDisplayTags(
feature.name,
);
} catch (e) {
_availableMorphTags = defaultMorphMapping.getDisplayTags(
feature.name,
);
} finally {
if (mounted) setState(() {});
}
}
void _saveChanges() {
if (_selectedMorphTag == null) return;
showFutureLoadingDialog(
context: context,
future: () => _sendEditedMessage(
(token) {
token.morph[widget.morphFeature] = _selectedMorphTag!;
if (widget.morphFeature.name.toLowerCase() == 'pos') {
token.pos = _selectedMorphTag!;
}
return token;
},
),
);
}
Future<void> _sendEditedMessage(
PangeaToken Function(PangeaToken token) changeCallback,
) async {
try {
final pm = widget.pangeaMessageEvent;
final existingTokens = pm.originalSent!.tokens!
.map((token) => PangeaToken.fromJson(token.toJson()))
.toList();
final tokenIndex = existingTokens.indexWhere(
(token) => token.text.offset == widget.token.text.offset,
);
if (tokenIndex == -1) {
throw Exception("Token not found in message");
}
existingTokens[tokenIndex] = changeCallback(existingTokens[tokenIndex]);
await pm.room.pangeaSendTextEvent(
pm.messageDisplayText,
editEventId: pm.eventId,
originalSent: pm.originalSent?.content,
originalWritten: pm.originalWritten?.content,
tokensSent: PangeaMessageTokens(
tokens: existingTokens,
detections: pm.originalSent?.detections,
),
tokensWritten: pm.originalWritten?.tokens != null
? PangeaMessageTokens(
tokens: pm.originalWritten!.tokens!,
detections: pm.originalWritten?.detections,
)
: null,
choreo: pm.originalSent?.choreo,
messageTag: ModelKey.messageTagMorphEdit,
);
widget.onClose();
} catch (e) {
ErrorHandler.logError(
e: e,
data: {
"selectedMorphTag": _selectedMorphTag,
"morphFeature": widget.morphFeature.name,
"pangeaMessageEvent": widget.pangeaMessageEvent.event.content,
},
);
}
}
bool get _canSaveChanges =>
_selectedMorphTag != _assignedMorphTag && _selectedMorphTag != null;
@override
Widget build(BuildContext context) {
return Stack(
children: [
Padding(
padding: const EdgeInsets.only(
top: 16.0,
bottom: 48.0,
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 8.0,
children: [
Text(
"${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).chooseCorrectLabel}",
textAlign: TextAlign.center,
style: const TextStyle(fontStyle: FontStyle.italic),
),
if (_availableMorphTags == null || _availableMorphTags!.isEmpty)
const CircularProgressIndicator()
else
Wrap(
alignment: WrapAlignment.center,
children: _availableMorphTags!.map((tag) {
return Container(
margin: const EdgeInsets.all(2),
padding: EdgeInsets.zero,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(10),
),
border: Border.all(
color: _selectedMorphTag == tag
? Theme.of(context).colorScheme.primary
: Colors.transparent,
style: BorderStyle.solid,
width: 2.0,
),
),
child: TextButton(
style: ButtonStyle(
padding: WidgetStateProperty.all(
const EdgeInsets.symmetric(
horizontal: 7,
),
),
backgroundColor: WidgetStateProperty.all<Color>(
_selectedMorphTag == tag
? Theme.of(context)
.colorScheme
.primary
.withAlpha(50)
: Colors.transparent,
),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
onPressed: () =>
setState(() => _selectedMorphTag = tag),
child: Text(
getGrammarCopy(
category: widget.morphFeature.name,
lemma: tag,
context: context,
) ??
tag,
textAlign: TextAlign.center,
),
),
);
}).toList(),
),
],
),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 10,
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
padding: const EdgeInsets.symmetric(horizontal: 10),
),
onPressed: widget.onClose,
child: Text(L10n.of(context).cancel),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
padding: const EdgeInsets.symmetric(horizontal: 10),
),
onPressed: _canSaveChanges ? _saveChanges : null,
child: Text(L10n.of(context).saveChanges),
),
],
),
),
),
],
);
}
}

View file

@ -1,10 +0,0 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
String getMorphSvgLink({
required String morphFeature,
String? morphTag,
required BuildContext context,
}) =>
"${AppConfig.assetsBaseURL}/${morphFeature.toLowerCase()}_${morphTag?.toLowerCase() ?? ''}.svg";

View file

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/common/widgets/customized_svg.dart';
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
import 'package:fluffychat/pangea/morphs/get_svg_link.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/utils/color_value.dart';
@ -20,6 +20,13 @@ class MorphIcon extends StatelessWidget {
final bool showTooltip;
final Size? size;
String getMorphSvgLink({
required String morphFeature,
String? morphTag,
required BuildContext context,
}) =>
"${AppConfig.assetsBaseURL}/${morphFeature.toLowerCase()}_${morphTag?.toLowerCase() ?? ''}.svg";
@override
Widget build(BuildContext context) {
// debugPrint("MorphIcon: morphFeature: $morphFeature, morphTag: $morphTag");

View file

@ -1,9 +1,7 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
import 'package:fluffychat/pangea/lemmas/lemma_edit_request.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_repo.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_request.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
@ -29,13 +27,10 @@ class LemmaMeaningBuilder extends StatefulWidget {
}
class LemmaMeaningBuilderState extends State<LemmaMeaningBuilder> {
bool editMode = false;
LemmaInfoResponse? lemmaInfo;
bool isLoading = true;
Object? error;
TextEditingController controller = TextEditingController();
@override
void initState() {
super.initState();
@ -51,12 +46,6 @@ class LemmaMeaningBuilderState extends State<LemmaMeaningBuilder> {
}
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
LemmaInfoRequest get _request => LemmaInfoRequest(
lemma: widget.constructId.lemma,
partOfSpeech: widget.constructId.category,
@ -75,7 +64,6 @@ class LemmaMeaningBuilderState extends State<LemmaMeaningBuilder> {
try {
final resp = await LemmaInfoRepo.get(_request);
lemmaInfo = resp;
controller.text = resp.meaning;
} catch (e) {
error = e;
} finally {
@ -83,42 +71,6 @@ class LemmaMeaningBuilderState extends State<LemmaMeaningBuilder> {
}
}
void toggleEditMode(bool value) => setState(() => editMode = value);
Future<void> editLemmaMeaning(String userEdit) async {
try {
await LemmaInfoRepo.edit(
LemmaEditRequest(
lemma: widget.constructId.lemma,
partOfSpeech: widget.constructId.category,
lemmaLang: widget.langCode,
userL1: MatrixState
.pangeaController.languageController.userL1?.langCode ??
LanguageKeys.defaultLanguage,
newMeaning: userEdit,
newEmojis: lemmaInfo?.emoji,
),
);
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
'lemma': widget.constructId.lemma,
'partOfSpeech': widget.constructId.category,
'lemmaLang': widget.langCode,
'userL1': MatrixState
.pangeaController.languageController.userL1?.langCode ??
LanguageKeys.defaultLanguage,
'newMeaning': userEdit,
},
);
} finally {
toggleEditMode(false);
_fetchLemmaMeaning();
}
}
@override
Widget build(BuildContext context) {
return widget.builder(

View file

@ -46,93 +46,22 @@ class LemmaMeaningWidget extends StatelessWidget {
);
}
if (controller.editMode) {
controller.controller.text = controller.lemmaInfo?.meaning ?? "";
return Material(
type: MaterialType.transparency,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).whatIsMeaning(
constructUse.lemma,
constructUse.category,
)}",
textAlign: TextAlign.center,
style: const TextStyle(fontStyle: FontStyle.italic),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: TextField(
minLines: 1,
maxLines: 3,
controller: controller.controller,
decoration: InputDecoration(
hintText: controller.lemmaInfo?.meaning,
),
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => controller.toggleEditMode(false),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
padding: const EdgeInsets.symmetric(horizontal: 10),
),
child: Text(L10n.of(context).cancel),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: () => controller.controller.text !=
controller.lemmaInfo?.meaning &&
controller.controller.text.isNotEmpty
? controller.editLemmaMeaning(controller.controller.text)
: null,
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
padding: const EdgeInsets.symmetric(horizontal: 10),
),
child: Text(L10n.of(context).saveChanges),
),
],
),
],
),
);
}
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Tooltip(
triggerMode: TooltipTriggerMode.tap,
message: L10n.of(context).doubleClickToEdit,
child: GestureDetector(
onLongPress: () => controller.toggleEditMode(true),
onDoubleTap: () => controller.toggleEditMode(true),
child: RichText(
textAlign: leading == null ? TextAlign.center : TextAlign.start,
text: TextSpan(
style: style,
children: [
if (leading != null) leading!,
if (leading != null)
const WidgetSpan(child: SizedBox(width: 6.0)),
TextSpan(
text: controller.lemmaInfo?.meaning,
),
],
child: RichText(
textAlign: leading == null ? TextAlign.center : TextAlign.start,
text: TextSpan(
style: style,
children: [
if (leading != null) leading!,
if (leading != null)
const WidgetSpan(child: SizedBox(width: 6.0)),
TextSpan(
text: controller.lemmaInfo?.meaning,
),
),
],
),
),
),

View file

@ -134,81 +134,6 @@ class WordZoomWidget extends StatelessWidget {
langCode: langCode,
constructId: construct,
builder: (context, controller) {
if (controller.editMode) {
return Column(
children: [
Text(
"${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).whatIsMeaning(
construct.lemma,
construct.category,
)}",
textAlign: TextAlign.center,
style: const TextStyle(
fontStyle: FontStyle.italic,
),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
child: TextField(
minLines: 1,
maxLines: 3,
controller: controller.controller,
decoration: InputDecoration(
hintText: controller.lemmaInfo?.meaning,
),
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () =>
controller.toggleEditMode(false),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(10.0),
),
padding: const EdgeInsets.symmetric(
horizontal: 10,
),
),
child: Text(L10n.of(context).cancel),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: () =>
controller.controller.text !=
controller.lemmaInfo
?.meaning &&
controller.controller.text
.isNotEmpty
? controller.editLemmaMeaning(
controller.controller.text,
)
: null,
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(10.0),
),
padding: const EdgeInsets.symmetric(
horizontal: 10,
),
),
child:
Text(L10n.of(context).saveChanges),
),
],
),
],
);
}
return Column(
spacing: 12.0,
mainAxisSize: MainAxisSize.min,
@ -246,44 +171,37 @@ class WordZoomWidget extends StatelessWidget {
controller.lemmaInfo == null)
const CircularProgressIndicator.adaptive()
else
GestureDetector(
onLongPress: () =>
controller.toggleEditMode(true),
onDoubleTap: () =>
controller.toggleEditMode(true),
child: construct.lemma.toLowerCase() ==
token.content.toLowerCase()
? Text(
controller.lemmaInfo!.meaning,
style:
const TextStyle(fontSize: 14.0),
textAlign: TextAlign.center,
)
: RichText(
text: TextSpan(
style:
DefaultTextStyle.of(context)
.style
.copyWith(
fontSize: 14.0,
),
children: [
TextSpan(text: construct.lemma),
const WidgetSpan(
child: SizedBox(width: 8.0),
construct.lemma.toLowerCase() ==
token.content.toLowerCase()
? Text(
controller.lemmaInfo!.meaning,
style:
const TextStyle(fontSize: 14.0),
textAlign: TextAlign.center,
)
: RichText(
text: TextSpan(
style: DefaultTextStyle.of(context)
.style
.copyWith(
fontSize: 14.0,
),
const TextSpan(text: ":"),
const WidgetSpan(
child: SizedBox(width: 8.0),
),
TextSpan(
text: controller
.lemmaInfo!.meaning,
),
],
),
children: [
TextSpan(text: construct.lemma),
const WidgetSpan(
child: SizedBox(width: 8.0),
),
const TextSpan(text: ":"),
const WidgetSpan(
child: SizedBox(width: 8.0),
),
TextSpan(
text: controller
.lemmaInfo!.meaning,
),
],
),
),
),
],
);
},