diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..59cdec5c9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "dart.previewLsp": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit", + "source.sortMembers": "never" + }, + "editor.formatOnSave": true +} \ No newline at end of file diff --git a/lib/pangea/analytics_details_popup/morph_details_view.dart b/lib/pangea/analytics_details_popup/morph_details_view.dart index 4f014e593..af38b7d49 100644 --- a/lib/pangea/analytics_details_popup/morph_details_view.dart +++ b/lib/pangea/analytics_details_popup/morph_details_view.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup_content.dart'; +import 'package:fluffychat/pangea/analytics_details_popup/morph_meaning_widget.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_identifier.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_level_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; @@ -102,23 +103,15 @@ class MorphDetailsView extends StatelessWidget { AsyncSnapshot snapshot, ) { if (snapshot.hasData) { - return RichText( - text: TextSpan( - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, + return MorphMeaningWidget( + feature: _morphFeature, + tag: _morphTag, + style: Theme.of(context).textTheme.bodyLarge, + leading: TextSpan( + text: L10n.of(context).meaningSectionHeader, + style: const TextStyle( + fontWeight: FontWeight.bold, ), - children: [ - TextSpan( - text: L10n.of(context).meaningSectionHeader, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - TextSpan( - text: " ${snapshot.data!}", - style: Theme.of(context).textTheme.bodyLarge, - ), - ], ), ); } else if (snapshot.hasError) { diff --git a/lib/pangea/analytics_details_popup/morph_meaning_widget.dart b/lib/pangea/analytics_details_popup/morph_meaning_widget.dart new file mode 100644 index 000000000..41e910b0c --- /dev/null +++ b/lib/pangea/analytics_details_popup/morph_meaning_widget.dart @@ -0,0 +1,227 @@ +import 'dart:developer'; +import 'dart:math'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart'; +import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; +import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_repo.dart'; + +class MorphMeaningWidget extends StatefulWidget { + final String feature; + final String tag; + final TextStyle? style; + final InlineSpan? leading; + + const MorphMeaningWidget({ + super.key, + required this.feature, + required this.tag, + this.style, + this.leading, + }); + + @override + MorphMeaningWidgetState createState() => MorphMeaningWidgetState(); +} + +class MorphMeaningWidgetState extends State { + bool _editMode = false; + late TextEditingController _controller; + static const int maxCharacters = 140; + String? _cachedResponse; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + Future _morphMeaning() async { + if (_cachedResponse != null) { + return _cachedResponse!; + } + + final response = await MorphInfoRepo.get( + feature: widget.feature, + tag: widget.tag, + ); + _cachedResponse = response; + return response ?? L10n.of(context).meaningNotFound; + } + + void _toggleEditMode(bool value) => setState(() => _editMode = value); + + Future editMorphMeaning(String userEdit) async { + // Truncate to max characters if needed + final truncatedEdit = userEdit.length > maxCharacters + ? userEdit.substring(0, maxCharacters) + : userEdit; + + await MorphInfoRepo.setMorphDefinition( + feature: widget.feature, + tag: widget.tag, + defintion: truncatedEdit, + ); + + // Update the cached response + _cachedResponse = truncatedEdit; + _toggleEditMode(false); + } + + void _setMeaningText(String initialText) { + _controller.text = initialText.substring( + 0, + min(initialText.length, maxCharacters), + ); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _morphMeaning(), + builder: (context, snapshot) { + if (snapshot.hasData) { + _setMeaningText(snapshot.data!); + } + + if (_editMode) { + return MorphEditView( + morphFeature: widget.feature, + morphTag: widget.tag, + meaning: snapshot.data ?? "", + controller: _controller, + toggleEditMode: _toggleEditMode, + editMorphMeaning: editMorphMeaning, + ); + } + + if (snapshot.connectionState != ConnectionState.done) { + return const TextLoadingShimmer(); + } + + if (snapshot.hasError || snapshot.data == null) { + debugger(when: kDebugMode); + return Text( + snapshot.error.toString(), + textAlign: TextAlign.center, + style: widget.style, + ); + } + + return Flexible( + child: Tooltip( + triggerMode: TooltipTriggerMode.tap, + message: L10n.of(context).doubleClickToEdit, + child: GestureDetector( + onLongPress: () => _toggleEditMode(true), + onDoubleTap: () => _toggleEditMode(true), + child: RichText( + text: TextSpan( + style: widget.style, + children: [ + if (widget.leading != null) widget.leading!, + if (widget.leading != null) const TextSpan(text: ' '), + TextSpan(text: snapshot.data!), + ], + ), + ), + ), + ), + ); + }, + ); + } +} + +class MorphEditView extends StatelessWidget { + final String morphFeature; + final String morphTag; + final String meaning; + final TextEditingController controller; + final void Function(bool) toggleEditMode; + final void Function(String) editMorphMeaning; + + const MorphEditView({ + required this.morphFeature, + required this.morphTag, + required this.meaning, + required this.controller, + required this.toggleEditMode, + required this.editMorphMeaning, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).whatIsMeaning( + getGrammarCopy( + category: morphFeature, + lemma: morphTag, + context: context, + ) ?? + morphTag, + '', + )}", + 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, + maxLength: MorphMeaningWidgetState.maxCharacters, + controller: controller, + ), + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => 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.text != meaning && controller.text.isNotEmpty + ? editMorphMeaning(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), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/pangea/analytics_details_popup/vocab_details_view.dart b/lib/pangea/analytics_details_popup/vocab_details_view.dart index 28715040d..b636f6a3a 100644 --- a/lib/pangea/analytics_details_popup/vocab_details_view.dart +++ b/lib/pangea/analytics_details_popup/vocab_details_view.dart @@ -140,26 +140,17 @@ class VocabDetailsView extends StatelessWidget { alignment: Alignment.topLeft, child: _userL2 == null ? Text(L10n.of(context).meaningNotFound) - : Wrap( - children: [ - Text( - L10n.of(context).meaningSectionHeader, - style: - Theme.of(context).textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.bold, - ), + : LemmaMeaningWidget( + text: _construct.lemma, + pos: _construct.category, + langCode: _userL2!, + style: Theme.of(context).textTheme.bodyLarge, + leading: TextSpan( + text: L10n.of(context).meaningSectionHeader, + style: const TextStyle( + fontWeight: FontWeight.bold, ), - const SizedBox(width: 8), - // Pass the lemma text and form correctly - // The lemma text is in _construct.lemma - // For the form, we use the same value since we don't have access to PangeaToken's form - LemmaMeaningWidget( - text: _construct.lemma, - pos: _construct.category, - langCode: _userL2!, - style: Theme.of(context).textTheme.bodyLarge, - ), - ], + ), ), ), ), diff --git a/lib/pangea/morphs/morph_meaning/morph_info_repo.dart b/lib/pangea/morphs/morph_meaning/morph_info_repo.dart index 11d988cd8..df90dcfde 100644 --- a/lib/pangea/morphs/morph_meaning/morph_info_repo.dart +++ b/lib/pangea/morphs/morph_meaning/morph_info_repo.dart @@ -96,4 +96,33 @@ class MorphInfoRepo { return res.getFeatureByCode(feature)?.getTagByCode(tag)?.l1Description; } + + static Future setMorphDefinition({ + required String feature, + required String tag, + required String defintion, + }) async { + final userL1 = + MatrixState.pangeaController.languageController.userL1?.langCode ?? + LanguageKeys.defaultLanguage; + final userL2 = + MatrixState.pangeaController.languageController.userL2?.langCode ?? + LanguageKeys.defaultLanguage; + final userL1Short = userL1.split('-').first; + final userL2Short = userL2.split('-').first; + final cachedJson = _morphMeaningStorage.read(userL1Short + userL2Short); + + MorphInfoResponse? resp = MorphInfoResponse( + userL1: userL1, + userL2: userL2, + features: [], + ); + + if (cachedJson is Map) { + resp = MorphInfoResponse.fromJson(cachedJson); + } + + resp.setMorphDefinition(feature, tag, defintion); + await _morphMeaningStorage.write(userL1Short + userL2Short, resp.toJson()); + } } diff --git a/lib/pangea/morphs/morph_meaning/morph_info_response.dart b/lib/pangea/morphs/morph_meaning/morph_info_response.dart index e734dfc95..a7e044651 100644 --- a/lib/pangea/morphs/morph_meaning/morph_info_response.dart +++ b/lib/pangea/morphs/morph_meaning/morph_info_response.dart @@ -3,7 +3,7 @@ import 'package:collection/collection.dart'; class MorphologicalTag { final String code; final String l1Title; - final String l1Description; + String l1Description; MorphologicalTag({ required this.code, @@ -103,4 +103,48 @@ class MorphInfoResponse { (feature) => feature.code.toLowerCase() == code.toLowerCase(), ); } + + void setMorphDefinition( + String morphFeature, + String morphTag, + String defintion, + ) { + final featureIndex = features.indexWhere( + (feature) => feature.code.toLowerCase() == morphFeature.toLowerCase(), + ); + + if (featureIndex == -1) { + features.add( + MorphologicalFeature( + code: morphFeature, + l1Title: morphFeature, + tags: [ + MorphologicalTag( + code: morphTag, + l1Title: morphTag, + l1Description: defintion, + ), + ], + ), + ); + return; + } + + final tagIndex = features[featureIndex].tags.indexWhere( + (tag) => tag.code.toLowerCase() == morphTag.toLowerCase(), + ); + + if (tagIndex == -1) { + features[featureIndex].tags.add( + MorphologicalTag( + code: morphTag, + l1Title: morphTag, + l1Description: defintion, + ), + ); + return; + } + + features[featureIndex].tags[tagIndex].l1Description = defintion; + } } diff --git a/lib/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart index e6a8314d6..15a8efe98 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart @@ -18,6 +18,7 @@ class LemmaMeaningWidget extends StatefulWidget { final String text; final String langCode; final TextStyle? style; + final InlineSpan? leading; const LemmaMeaningWidget({ super.key, @@ -25,6 +26,7 @@ class LemmaMeaningWidget extends StatefulWidget { required this.text, required this.langCode, this.style, + this.leading, }); @override @@ -139,10 +141,15 @@ class LemmaMeaningWidgetState extends State { child: GestureDetector( onLongPress: () => _toggleEditMode(true), onDoubleTap: () => _toggleEditMode(true), - child: Text( - snapshot.data!.meaning, - textAlign: TextAlign.center, - style: widget.style, + child: RichText( + text: TextSpan( + style: widget.style, + children: [ + if (widget.leading != null) widget.leading!, + if (widget.leading != null) const TextSpan(text: ' '), + TextSpan(text: snapshot.data!.meaning), + ], + ), ), ), ),