feat: underline new tokens and animate collecting them on click
This commit is contained in:
parent
9a4bb6e88c
commit
bf29b28364
8 changed files with 237 additions and 28 deletions
|
|
@ -4589,6 +4589,7 @@
|
|||
"constructUseIncMDesc": "Incorrect in grammar activity",
|
||||
"constructUseIgnMDesc": "Ignored in grammar activity",
|
||||
"constructUseEmojiDesc": "Correct in emoji activity",
|
||||
"constructUseCollected": "Collected in chat",
|
||||
"constructUseNanDesc": "Not applicable",
|
||||
"xpIntoLevel": "{currentXP} / {maxXP} XP",
|
||||
"@xpIntoLevel": {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,4 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_highlighter/flutter_highlighter.dart';
|
||||
import 'package:flutter_highlighter/themes/shades-of-purple.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:html/dom.dart' as dom;
|
||||
import 'package:html/parser.dart' as parser;
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/any_state_holder.dart';
|
||||
|
|
@ -24,6 +14,15 @@ import 'package:fluffychat/widgets/avatar.dart';
|
|||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_highlighter/flutter_highlighter.dart';
|
||||
import 'package:flutter_highlighter/themes/shades-of-purple.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:html/dom.dart' as dom;
|
||||
import 'package:html/parser.dart' as parser;
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../../utils/url_launcher.dart';
|
||||
|
||||
class HtmlMessage extends StatelessWidget {
|
||||
|
|
@ -324,6 +323,10 @@ class HtmlMessage extends StatelessWidget {
|
|||
? isSelected!.call(token)
|
||||
: false;
|
||||
|
||||
final isNew = token != null &&
|
||||
overlayController != null &&
|
||||
overlayController!.isNewToken(token);
|
||||
|
||||
final tokenWidth = renderer.tokenTextWidthForContainer(
|
||||
context,
|
||||
node.text,
|
||||
|
|
@ -352,6 +355,7 @@ class HtmlMessage extends StatelessWidget {
|
|||
color: renderer.backgroundColor(
|
||||
context,
|
||||
selected,
|
||||
isNew,
|
||||
),
|
||||
),
|
||||
width: tokenWidth,
|
||||
|
|
@ -386,6 +390,7 @@ class HtmlMessage extends StatelessWidget {
|
|||
color: renderer.backgroundColor(
|
||||
context,
|
||||
selected,
|
||||
isNew,
|
||||
),
|
||||
),
|
||||
linkStyle: linkStyle,
|
||||
|
|
@ -528,6 +533,7 @@ class HtmlMessage extends StatelessWidget {
|
|||
color: renderer.backgroundColor(
|
||||
context,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -543,6 +549,7 @@ class HtmlMessage extends StatelessWidget {
|
|||
color: renderer.backgroundColor(
|
||||
context,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
),
|
||||
// Pangea#
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/analytics_downloads/space_analytics_summary_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum ConstructUseTypeEnum {
|
||||
/// produced in chat by user, igc was run, and we've judged it to be a correct use
|
||||
|
|
@ -66,6 +65,9 @@ enum ConstructUseTypeEnum {
|
|||
incMM,
|
||||
ignMM,
|
||||
|
||||
/// lemma collected by clicking on it
|
||||
click,
|
||||
|
||||
/// not defined, likely a new construct introduced by choreo and not yet classified by an old version of the client
|
||||
nan
|
||||
}
|
||||
|
|
@ -135,6 +137,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
return L10n.of(context).constructUseIncMmDesc;
|
||||
case ConstructUseTypeEnum.ignMM:
|
||||
return L10n.of(context).constructUseIgnMmDesc;
|
||||
case ConstructUseTypeEnum.click:
|
||||
return L10n.of(context).constructUseCollected;
|
||||
case ConstructUseTypeEnum.nan:
|
||||
return L10n.of(context).constructUseNanDesc;
|
||||
}
|
||||
|
|
@ -185,6 +189,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.unk:
|
||||
case ConstructUseTypeEnum.nan:
|
||||
return Icons.help;
|
||||
case ConstructUseTypeEnum.click:
|
||||
return Icons.format_color_text;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -211,6 +217,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
|
||||
case ConstructUseTypeEnum.corIGC:
|
||||
case ConstructUseTypeEnum.corL:
|
||||
case ConstructUseTypeEnum.click:
|
||||
return 2;
|
||||
|
||||
case ConstructUseTypeEnum.corIt:
|
||||
|
|
@ -279,6 +286,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.incMM:
|
||||
case ConstructUseTypeEnum.ignMM:
|
||||
case ConstructUseTypeEnum.em:
|
||||
case ConstructUseTypeEnum.click:
|
||||
case ConstructUseTypeEnum.nan:
|
||||
return false;
|
||||
}
|
||||
|
|
@ -318,6 +326,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.incMM:
|
||||
case ConstructUseTypeEnum.ignMM:
|
||||
case ConstructUseTypeEnum.em:
|
||||
case ConstructUseTypeEnum.click:
|
||||
return LearningSkillsEnum.reading;
|
||||
case ConstructUseTypeEnum.pvm:
|
||||
return LearningSkillsEnum.speaking;
|
||||
|
|
@ -364,6 +373,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.ignL:
|
||||
case ConstructUseTypeEnum.ignM:
|
||||
case ConstructUseTypeEnum.ignMM:
|
||||
case ConstructUseTypeEnum.click:
|
||||
case ConstructUseTypeEnum.nan:
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TokenRenderingUtil {
|
||||
final PangeaMessageEvent? pangeaMessageEvent;
|
||||
|
|
@ -85,7 +85,10 @@ class TokenRenderingUtil {
|
|||
}
|
||||
}
|
||||
|
||||
Color backgroundColor(BuildContext context, bool selected) {
|
||||
Color backgroundColor(BuildContext context, bool selected, bool isNew) {
|
||||
if (isNew) {
|
||||
return AppConfig.success;
|
||||
}
|
||||
return selected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.white.withAlpha(0);
|
||||
|
|
|
|||
|
|
@ -2,15 +2,13 @@ import 'dart:async';
|
|||
import 'dart:developer';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/overlay.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
|
|
@ -35,6 +33,10 @@ import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/morph_sel
|
|||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_positioner.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/reading_assistance_content.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
/// Controls data at the top level of the toolbar (mainly token / toolbar mode selection)
|
||||
class MessageSelectionOverlay extends StatefulWidget {
|
||||
|
|
@ -103,6 +105,8 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
|
||||
double maxWidth = AppConfig.toolbarMinWidth;
|
||||
|
||||
List<PangeaToken> newTokens = [];
|
||||
|
||||
/////////////////////////////////////
|
||||
/// Lifecycle
|
||||
/////////////////////////////////////
|
||||
|
|
@ -114,6 +118,12 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => widget.chatController.setSelectedEvent(event),
|
||||
);
|
||||
newTokens = pangeaMessageEvent?.messageDisplayRepresentation?.tokens
|
||||
?.where((token) {
|
||||
return token.lemma.saveVocab == true &&
|
||||
token.vocabConstruct.uses.isEmpty;
|
||||
}).toList() ??
|
||||
[];
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -569,6 +579,42 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
}
|
||||
|
||||
updateSelectedSpan(token.text);
|
||||
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (isNewToken(token)) {
|
||||
MatrixState.pangeaController.putAnalytics.setState(
|
||||
AnalyticsStream(
|
||||
eventId: event.eventId,
|
||||
roomId: event.room.id,
|
||||
constructs: [
|
||||
OneConstructUse(
|
||||
useType: ConstructUseTypeEnum.click,
|
||||
lemma: token.lemma.text,
|
||||
constructType: ConstructTypeEnum.vocab,
|
||||
metadata: ConstructUseMetaData(
|
||||
roomId: event.room.id,
|
||||
timeStamp: DateTime.now(),
|
||||
eventId: event.eventId,
|
||||
),
|
||||
category: token.pos,
|
||||
form: token.text.content,
|
||||
xp: ConstructUseTypeEnum.click.pointValue,
|
||||
),
|
||||
],
|
||||
targetID: token.text.uniqueKey,
|
||||
),
|
||||
);
|
||||
// Remove the token from newTokens so it is no longer highlighted as "new"
|
||||
setState(() {
|
||||
newTokens.removeWhere(
|
||||
(t) =>
|
||||
t.text.offset == token.text.offset &&
|
||||
t.text.length == token.text.length,
|
||||
);
|
||||
debugPrint("$token.text has been removed from newTokens list.");
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Whether the given token is currently selected or highlighted
|
||||
|
|
@ -578,6 +624,15 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
return isSelected;
|
||||
}
|
||||
|
||||
bool isNewToken(PangeaToken token) {
|
||||
if (newTokens.isEmpty) return false;
|
||||
return newTokens.any(
|
||||
(t) =>
|
||||
t.text.offset == token.text.offset &&
|
||||
t.text.length == token.text.length,
|
||||
);
|
||||
}
|
||||
|
||||
bool isTokenHighlighted(PangeaToken token) {
|
||||
if (_highlightedTokens == null) return false;
|
||||
return _highlightedTokens!.any(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix_api_lite/model/message_types.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
|
|
@ -16,6 +12,8 @@ import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/practice_act
|
|||
import 'package:fluffychat/pangea/toolbar/widgets/toolbar_content_loading_indicator.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix_api_lite/model/message_types.dart';
|
||||
|
||||
const double minCardHeight = 70;
|
||||
|
||||
|
|
@ -120,6 +118,8 @@ class ReadingAssistanceContentState extends State<ReadingAssistanceContent> {
|
|||
token: widget.overlayController.selectedToken!,
|
||||
messageEvent: widget.overlayController.pangeaMessageEvent!,
|
||||
overlayController: widget.overlayController,
|
||||
wordIsNew: widget.overlayController
|
||||
.isNewToken(widget.overlayController.selectedToken!),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
121
lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart
Normal file
121
lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NewWordOverlay extends StatefulWidget {
|
||||
final Widget child;
|
||||
final bool show;
|
||||
final Color overlayColor;
|
||||
final VoidCallback? onComplete;
|
||||
|
||||
const NewWordOverlay({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.show,
|
||||
required this.overlayColor,
|
||||
this.onComplete,
|
||||
});
|
||||
|
||||
@override
|
||||
State<NewWordOverlay> createState() => _NewWordOverlayState();
|
||||
}
|
||||
|
||||
class _NewWordOverlayState extends State<NewWordOverlay>
|
||||
with TickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<double> _xpScaleAnim;
|
||||
late final Animation<double> _fadeAnim;
|
||||
late final Animation<Alignment> _alignmentAnim;
|
||||
late final Animation<Offset> _offsetAnim;
|
||||
final bool _showOverlay = true;
|
||||
Widget xpSeedWidget = const SizedBox();
|
||||
Widget? get svg => ConstructLevelEnum.seeds.icon();
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 2500),
|
||||
);
|
||||
|
||||
_xpScaleAnim = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.bounceOut),
|
||||
);
|
||||
_fadeAnim = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.7, 1.0, curve: Curves.easeOut),
|
||||
);
|
||||
|
||||
// Alignment animation: stays center until 0.5, then animates to topRight
|
||||
_alignmentAnim = AlignmentTween(
|
||||
begin: Alignment.center,
|
||||
end: Alignment.topRight,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.5, 1.0, curve: Curves.easeInOut),
|
||||
),
|
||||
);
|
||||
|
||||
// Offset animation: stays at Offset.zero, then moves up and right
|
||||
_offsetAnim = Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: const Offset(-0.1, -0.1),
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.5, 1.0, curve: Curves.easeInOut),
|
||||
),
|
||||
);
|
||||
|
||||
xpSeedWidget = Container(
|
||||
child: svg,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
_controller.forward();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_showOverlay) return widget.child;
|
||||
return Stack(
|
||||
children: [
|
||||
widget.child,
|
||||
Positioned.fill(
|
||||
child: FadeTransition(
|
||||
opacity: ReverseAnimation(_fadeAnim),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
color: widget.overlayColor,
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Align(
|
||||
alignment: _alignmentAnim.value,
|
||||
child: FractionalTranslation(
|
||||
translation: _offsetAnim.value,
|
||||
child: ScaleTransition(
|
||||
scale: _xpScaleAnim,
|
||||
child: xpSeedWidget,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart';
|
||||
|
|
@ -15,18 +13,22 @@ import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
|||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_audio_button.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_builder.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WordZoomWidget extends StatelessWidget {
|
||||
final PangeaToken token;
|
||||
final PangeaMessageEvent messageEvent;
|
||||
final MessageOverlayController overlayController;
|
||||
final bool wordIsNew;
|
||||
|
||||
const WordZoomWidget({
|
||||
super.key,
|
||||
required this.token,
|
||||
required this.messageEvent,
|
||||
required this.overlayController,
|
||||
required this.wordIsNew,
|
||||
});
|
||||
|
||||
PangeaToken get _selectedToken => overlayController.selectedToken!;
|
||||
|
|
@ -41,7 +43,9 @@ class WordZoomWidget extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
final isNewWord = wordIsNew;
|
||||
final overlayColor = Theme.of(context).scaffoldBackgroundColor;
|
||||
Widget card = Container(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: AppConfig.toolbarMinHeight - 8,
|
||||
|
|
@ -241,5 +245,13 @@ class WordZoomWidget extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
);
|
||||
if (isNewWord) {
|
||||
card = NewWordOverlay(
|
||||
show: true,
|
||||
overlayColor: overlayColor,
|
||||
child: card,
|
||||
);
|
||||
}
|
||||
return card;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue