feat: allow users to edit morph meanings (#2047)
This commit is contained in:
parent
714613f871
commit
b8e7eca69c
7 changed files with 340 additions and 40 deletions
9
.vscode/settings.json
vendored
Normal file
9
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"dart.previewLsp": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit",
|
||||
"source.organizeImports": "explicit",
|
||||
"source.sortMembers": "never"
|
||||
},
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
|
|
@ -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<String?> 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>[
|
||||
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) {
|
||||
|
|
|
|||
227
lib/pangea/analytics_details_popup/morph_meaning_widget.dart
Normal file
227
lib/pangea/analytics_details_popup/morph_meaning_widget.dart
Normal file
|
|
@ -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<MorphMeaningWidget> {
|
||||
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<String> _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<void> 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<String>(
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -96,4 +96,33 @@ class MorphInfoRepo {
|
|||
|
||||
return res.getFeatureByCode(feature)?.getTagByCode(tag)?.l1Description;
|
||||
}
|
||||
|
||||
static Future<void> 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<String, dynamic>) {
|
||||
resp = MorphInfoResponse.fromJson(cachedJson);
|
||||
}
|
||||
|
||||
resp.setMorphDefinition(feature, tag, defintion);
|
||||
await _morphMeaningStorage.write(userL1Short + userL2Short, resp.toJson());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<LemmaMeaningWidget> {
|
|||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue