From 1e20d5fb2c7a90ab67d9940f5a9ba9ce86866625 Mon Sep 17 00:00:00 2001 From: Sofanyas Genene <123987957+Sofanyas@users.noreply.github.com> Date: Tue, 29 Apr 2025 09:09:03 -0400 Subject: [PATCH] style and functionality changes to level up notification (#2444) * style and functionality changes to level up notification * generated * chore: return construct summary directly from function instead of waiting for state event to be sent * generated * XP animation bug, asking wilson to take a look * updated XP attributes but still facing XP retrieval bug * generated * Added new DinoBot image * updated dinoBot image, added padding on sides to table row, fixed duplicate variable naming error * generated * chore: some updates to simplify level up widget * chore: remove dino asset from pubspec.yaml * chore: revert testing changes to analytics controller * See details categories do not display unless XP gained above threshold * generated * chore: update icons in construct update popup above messages --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: ggurdin --- assets/l10n/intl_en.arb | 10 +- lib/pages/chat/chat.dart | 1 - .../analytics_details_popup_content.dart | 33 +- .../get_analytics_controller.dart | 183 ++++--- .../analytics_misc/learning_skills_enum.dart | 36 +- lib/pangea/analytics_misc/level_up.dart | 458 ++++++++++++++---- .../chat/utils/unlocked_morphs_snackbar.dart | 11 +- .../igc/message_analytics_feedback.dart | 7 +- lib/pangea/common/utils/any_state_holder.dart | 16 + lib/pangea/constructs/construct_repo.dart | 26 +- 10 files changed, 562 insertions(+), 219 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 6e107bcc4..49acb06fb 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4596,9 +4596,9 @@ "meaningSectionHeader": "Meaning:", "formSectionHeader": "Forms used in chats:", "noEmojiSelectedTooltip": "No emoji selected", - "writingExercisesTooltip": "Writing activities", - "listeningExercisesTooltip": "Listening activities", - "readingExercisesTooltip": "Reading activities", + "writingExercisesTooltip": "Writing practice", + "listeningExercisesTooltip": "Listening practice", + "readingExercisesTooltip": "Reading practice", "meaningNotFound": "Meaning could not be found.", "formsNotFound": "Forms could not be found.", "chooseBaseForm": "Choose the base form", @@ -4875,5 +4875,7 @@ "languageLevelC1Desc": "I can express ideas fluently and spontaneously without much struggle and understand a wide range of demanding texts.", "languageLevelC2Desc": "I can understand virtually everything heard or read and express myself fluently and precisely.", "newVocab": "New vocab", - "newGrammar": "New grammar concepts" + "newGrammar": "New grammar concepts", + "congratulationsOnReaching": "Congratulations on reaching ", + "seeDetails": "See Details" } \ No newline at end of file diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 8a6d6a4d3..667e5afe9 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -391,7 +391,6 @@ class ChatController extends State LevelUpUtil.showLevelUpDialog( update['level_up'], update['analytics_room_id'], - update["construct_summary_state_event_id"], update['construct_summary'], context, ); diff --git a/lib/pangea/analytics_details_popup/analytics_details_popup_content.dart b/lib/pangea/analytics_details_popup/analytics_details_popup_content.dart index f31030e4b..ca8eefbe3 100644 --- a/lib/pangea/analytics_details_popup/analytics_details_popup_content.dart +++ b/lib/pangea/analytics_details_popup/analytics_details_popup_content.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:material_symbols_icons/symbols.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/analytics_details_popup/lemma_usage_dots.dart'; @@ -73,27 +71,16 @@ class AnalyticsDetailsViewContent extends StatelessWidget { child: Column( children: [ LemmaUseExampleMessages(construct: construct), - // Writing exercise section - LemmaUsageDots( - construct: construct, - category: LearningSkillsEnum.writing, - tooltip: L10n.of(context).writingExercisesTooltip, - icon: Symbols.edit_square, - ), - // Listening exercise section - LemmaUsageDots( - construct: construct, - category: LearningSkillsEnum.hearing, - tooltip: L10n.of(context).listeningExercisesTooltip, - icon: Icons.volume_up, - ), - // Reading exercise section - LemmaUsageDots( - construct: construct, - category: LearningSkillsEnum.reading, - tooltip: L10n.of(context).readingExercisesTooltip, - icon: Symbols.two_pager, - ), + ...LearningSkillsEnum.values + .where((v) => v.isVisible) + .map((skill) { + return LemmaUsageDots( + construct: construct, + category: skill, + tooltip: skill.tooltip(context), + icon: skill.icon, + ); + }), ], ), ), diff --git a/lib/pangea/analytics_misc/get_analytics_controller.dart b/lib/pangea/analytics_misc/get_analytics_controller.dart index 8d6a26b68..272c181e2 100644 --- a/lib/pangea/analytics_misc/get_analytics_controller.dart +++ b/lib/pangea/analytics_misc/get_analytics_controller.dart @@ -17,6 +17,8 @@ import 'package:fluffychat/pangea/common/controllers/base_controller.dart'; import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; +import 'package:fluffychat/pangea/constructs/construct_repo.dart'; +import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; import 'package:fluffychat/pangea/practice_activities/practice_selection_repo.dart'; @@ -199,15 +201,14 @@ class GetAnalyticsController extends BaseController { ); Future _onLevelUp(final int lowerLevel, final int upperLevel) async { - // final result = await _generateLevelUpAnalyticsAndSaveToStateEvent( - // lowerLevel, - // upperLevel, - // ); + final result = await _generateLevelUpAnalyticsAndSaveToStateEvent( + lowerLevel, + upperLevel, + ); setState({ 'level_up': constructListModel.level, - // 'analytics_room_id': _client.analyticsRoomLocal(_l2!)?.id, - // "construct_summary_state_event_id": result?.stateEventId, - // "construct_summary": result?.summary, + 'analytics_room_id': _client.analyticsRoomLocal(_l2!)?.id, + "construct_summary": result, }); } @@ -398,6 +399,19 @@ class GetAnalyticsController extends BaseController { _cache.add(entry); } + Future _saveConstructSummaryResponseToStateEvent( + final ConstructSummary summary, + ) async { + final Room? analyticsRoom = _client.analyticsRoomLocal(_l2!); + final stateEventId = await _client.setRoomStateWithKey( + analyticsRoom!.id, + PangeaEventTypes.constructSummary, + '', + summary.toJson(), + ); + return stateEventId; + } + int newConstructCount( List newConstructs, ConstructTypeEnum type, @@ -434,76 +448,97 @@ class GetAnalyticsController extends BaseController { // int diffXP = maxXP - minXP; // if (diffXP < 0) diffXP = 0; -// // compute construct use of current level -// final List constructUseOfCurrentLevel = []; -// int score = 0; -// for (final use in constructListModel.uses) { -// constructUseOfCurrentLevel.add(use); -// score += use.pointValue; -// if (score >= diffXP) break; -// } + Future getConstructSummaryFromStateEvent() async { + try { + final Room? analyticsRoom = _client.analyticsRoomLocal(_l2!); + if (analyticsRoom == null) return null; + final state = + analyticsRoom.getState(PangeaEventTypes.constructSummary, ''); + if (state == null) return null; + return ConstructSummary.fromJson(state.content); + } catch (e) { + debugPrint("Error getting construct summary room: $e"); + ErrorHandler.logError(e: e, data: {'e': e}); + return null; + } + } -// // extract construct use message bodies for analytics -// List? constructUseMessageContentBodies = []; -// for (final use in constructUseOfCurrentLevel) { -// try { -// final useMessage = await use.getEvent(_client); -// final useMessageBody = useMessage?.content["body"]; -// if (useMessageBody is String) { -// constructUseMessageContentBodies.add(useMessageBody); -// } else { -// constructUseMessageContentBodies.add(null); -// } -// } catch (e) { -// constructUseMessageContentBodies.add(null); -// } -// } -// if (constructUseMessageContentBodies.length != -// constructUseOfCurrentLevel.length) { -// constructUseMessageContentBodies = null; -// } + Future _generateLevelUpAnalyticsAndSaveToStateEvent( + final int lowerLevel, + final int upperLevel, + ) async { + // generate level up analytics as a construct summary + ConstructSummary summary; + try { + final int maxXP = constructListModel.calculateXpWithLevel(upperLevel); + final int minXP = constructListModel.calculateXpWithLevel(lowerLevel); + int diffXP = maxXP - minXP; + if (diffXP < 0) diffXP = 0; -// final request = ConstructSummaryRequest( -// constructs: constructUseOfCurrentLevel, -// constructUseMessageContentBodies: constructUseMessageContentBodies, -// language: _l2!.langCodeShort, -// upperLevel: upperLevel, -// lowerLevel: lowerLevel, -// ); + // compute construct use of current level + final List constructUseOfCurrentLevel = []; + int score = 0; + for (final use in constructListModel.uses) { + constructUseOfCurrentLevel.add(use); + score += use.xp; + if (score >= diffXP) break; + } -// final response = await ConstructRepo.generateConstructSummary(request); -// summary = response.summary; -// } catch (e) { -// debugPrint("Error generating level up analytics: $e"); -// ErrorHandler.logError(e: e, data: {'e': e}); -// return null; -// } -// String stateEventId; -// try { -// final Room? analyticsRoom = _client.analyticsRoomLocal(_l2!); -// if (analyticsRoom == null) { -// ErrorHandler.logError( -// e: e, -// data: {'e': e, 'message': "Analytics room not found for user"}, -// ); -// return null; -// } -// stateEventId = await _client.setRoomStateWithKey( -// analyticsRoom.id, -// PangeaEventTypes.constructSummary, -// '', -// summary.toJson(), -// ); -// } catch (e) { -// debugPrint("Error saving construct summary room: $e"); -// ErrorHandler.logError(e: e, data: {'e': e}); -// return null; -// } -// return GenerateConstructSummaryResult( -// stateEventId: stateEventId, -// summary: summary, -// ); -// } + // extract construct use message bodies for analytics + List? constructUseMessageContentBodies = []; + for (final use in constructUseOfCurrentLevel) { + try { + final useMessage = await use.getEvent(_client); + final useMessageBody = useMessage?.content["body"]; + if (useMessageBody is String) { + constructUseMessageContentBodies.add(useMessageBody); + } else { + constructUseMessageContentBodies.add(null); + } + } catch (e) { + constructUseMessageContentBodies.add(null); + } + } + if (constructUseMessageContentBodies.length != + constructUseOfCurrentLevel.length) { + constructUseMessageContentBodies = null; + } + + final request = ConstructSummaryRequest( + constructs: constructUseOfCurrentLevel, + constructUseMessageContentBodies: constructUseMessageContentBodies, + language: _l2!.langCodeShort, + upperLevel: upperLevel, + lowerLevel: lowerLevel, + ); + + final response = await ConstructRepo.generateConstructSummary(request); + summary = response.summary; + } catch (e) { + debugPrint("Error generating level up analytics: $e"); + ErrorHandler.logError(e: e, data: {'e': e}); + return null; + } + try { + final Room? analyticsRoom = _client.analyticsRoomLocal(_l2!); + if (analyticsRoom == null) { + ErrorHandler.logError( + data: {'message': "Analytics room not found for user"}, + ); + return null; + } + + // don't await this, just return the original response + _saveConstructSummaryResponseToStateEvent( + summary, + ); + } catch (e) { + debugPrint("Error saving construct summary room: $e"); + ErrorHandler.logError(e: e, data: {'e': e}); + return null; + } + return summary; + } } class AnalyticsCacheEntry { diff --git a/lib/pangea/analytics_misc/learning_skills_enum.dart b/lib/pangea/analytics_misc/learning_skills_enum.dart index bf9c62042..0812f6656 100644 --- a/lib/pangea/analytics_misc/learning_skills_enum.dart +++ b/lib/pangea/analytics_misc/learning_skills_enum.dart @@ -1,7 +1,33 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:material_symbols_icons/symbols.dart'; + enum LearningSkillsEnum { - writing, - reading, - speaking, - hearing, - other, + writing(isVisible: true, icon: Symbols.edit_square), + reading(isVisible: true, icon: Symbols.two_pager), + speaking(isVisible: false), + hearing(isVisible: true, icon: Icons.volume_up), + other(isVisible: false); + + final bool isVisible; + final IconData icon; + + const LearningSkillsEnum({ + required this.isVisible, + this.icon = Icons.question_mark, + }); + + String tooltip(BuildContext context) { + switch (this) { + case LearningSkillsEnum.writing: + return L10n.of(context).writingExercisesTooltip; + case LearningSkillsEnum.reading: + return L10n.of(context).readingExercisesTooltip; + case LearningSkillsEnum.hearing: + return L10n.of(context).listeningExercisesTooltip; + default: + return ""; + } + } } diff --git a/lib/pangea/analytics_misc/level_up.dart b/lib/pangea/analytics_misc/level_up.dart index 36f33075d..e3973b584 100644 --- a/lib/pangea/analytics_misc/level_up.dart +++ b/lib/pangea/analytics_misc/level_up.dart @@ -1,161 +1,425 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:audioplayers/audioplayers.dart'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:http/http.dart' as http; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; +import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart'; import 'package:fluffychat/pangea/constructs/construct_repo.dart'; -import 'level_summary.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class LevelUpConstants { + static const String starFileName = "star.png"; + static const String dinoLevelUPFileName = "DinoBot-Congratulate.png"; +} class LevelUpUtil { - static void showLevelUpDialog( + static Future showLevelUpDialog( int level, String? analyticsRoomId, - String? summaryStateEventId, ConstructSummary? constructSummary, BuildContext context, - ) { + ) async { final player = AudioPlayer(); + + final snackbarRegex = RegExp(r'_snackbar$'); + + while (MatrixState.pAnyState.activeOverlays + .any((overlayId) => snackbarRegex.hasMatch(overlayId))) { + await Future.delayed(const Duration(milliseconds: 100)); + } + player.play( UrlSource( "${AppConfig.assetsBaseURL}/${AnalyticsConstants.levelUpAudioFileName}", ), ); - showDialog( - context: context, - builder: (context) => LevelUpAnimation( + final ValueNotifier showDetailsClicked = ValueNotifier(false); + + late final OverlayEntry overlayEntry; + overlayEntry = OverlayEntry( + builder: (context) => LevelUpBanner( level: level, - analyticsRoomId: analyticsRoomId, - summaryStateEventId: summaryStateEventId, constructSummary: constructSummary, + onDetailsClicked: () { + showDetailsClicked.value = true; + }, + onOverlayExit: () { + overlayEntry.remove(); + player.dispose(); + }, ), - ).then((_) => player.dispose()); + ); + + Overlay.of(context).insert(overlayEntry); + + Future.delayed(const Duration(seconds: 5), () { + if (!showDetailsClicked.value) { + overlayEntry.remove(); + player.dispose(); + } + }); } } -class LevelUpAnimation extends StatefulWidget { +class LevelUpBanner extends StatefulWidget { final int level; - final String? analyticsRoomId; - final String? summary; - final String? summaryStateEventId; final ConstructSummary? constructSummary; + final VoidCallback onDetailsClicked; + final VoidCallback onOverlayExit; - const LevelUpAnimation({ + const LevelUpBanner({ required this.level, - required this.analyticsRoomId, - this.summary, - this.summaryStateEventId, this.constructSummary, + required this.onDetailsClicked, + required this.onOverlayExit, super.key, }); @override - LevelUpAnimationState createState() => LevelUpAnimationState(); + LevelUpBannerState createState() => LevelUpBannerState(); } -class LevelUpAnimationState extends State { - Uint8List? bytes; - final imageURL = - "${AppConfig.assetsBaseURL}/${AnalyticsConstants.levelUpImageFileName}"; +class LevelUpBannerState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _slideAnimation; + bool _showDetails = false; @override void initState() { super.initState(); - _loadImageData().catchError((e) { - if (mounted) Navigator.of(context).pop(); - }); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 500), + ); + + _slideAnimation = Tween( + begin: const Offset(0, -1), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeOut, + ), + ); + + _controller.forward(); } @override void dispose() { + _controller.dispose(); super.dispose(); } - Future _loadImageData() async { - final resp = - await http.get(Uri.parse(imageURL)).timeout(const Duration(seconds: 5)); - if (resp.statusCode != 200) return; - if (mounted) { - setState(() => bytes = resp.bodyBytes); + int _skillsPoints(LearningSkillsEnum skill) { + switch (skill) { + case LearningSkillsEnum.writing: + return widget.constructSummary?.writingConstructScore ?? 0; + case LearningSkillsEnum.reading: + return widget.constructSummary?.readingConstructScore ?? 0; + case LearningSkillsEnum.speaking: + return widget.constructSummary?.speakingConstructScore ?? 0; + case LearningSkillsEnum.hearing: + return widget.constructSummary?.hearingConstructScore ?? 0; + default: + return 0; } } @override Widget build(BuildContext context) { - if (bytes == null) { - return const SizedBox(); - } - - return Dialog( - backgroundColor: Colors.transparent, - child: Stack( - alignment: Alignment.center, - children: [ - // Banner image - Image.memory( - bytes!, - height: kIsWeb ? 350 : 250, - width: double.infinity, - fit: BoxFit.cover, + return Stack( + children: [ + if (_showDetails) + GestureDetector( + onTap: () { + setState(() { + _showDetails = false; + }); + widget.onOverlayExit(); + }, + child: Container( + color: Colors.black.withAlpha(180), + ), ), - // Overlay: centered title and close button - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only( - top: kIsWeb ? 200 : 100, - ), // Added hardcoded padding above the text - child: Text( - L10n.of(context).levelPopupTitle(widget.level), - style: const TextStyle( - fontSize: kIsWeb ? 40 : 24, - fontWeight: FontWeight.bold, - color: Colors.white, + SlideTransition( + position: _slideAnimation, + child: Align( + alignment: Alignment.topCenter, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.5, + maxHeight: MediaQuery.of(context).size.height * 0.75, + ), + margin: const EdgeInsets.only(top: 16), + decoration: BoxDecoration( + color: widget.level > 10 + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 24, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + RichText( + text: TextSpan( + children: [ + TextSpan( + text: L10n.of(context).congratulationsOnReaching, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + TextSpan( + text: "${L10n.of(context).level} ", + style: const TextStyle( + color: AppConfig.gold, + fontSize: 18, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + TextSpan( + text: "${widget.level} ", + style: const TextStyle( + color: AppConfig.gold, + fontSize: 18, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + WidgetSpan( + child: CachedNetworkImage( + imageUrl: + "${AppConfig.assetsBaseURL}/${LevelUpConstants.starFileName}", + height: 24, + width: 24, + ), + ), + ], + ), + ), + ElevatedButton( + onPressed: () { + setState(() { + _showDetails = !_showDetails; + }); + widget.onDetailsClicked(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Theme.of(context) + .colorScheme + .surfaceContainerHighest, + ), + child: Row( + children: [ + Text( + "${L10n.of(context).seeDetails} ", + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Container( + decoration: const BoxDecoration( + color: AppConfig.gold, + shape: BoxShape.circle, + ), + padding: const EdgeInsets.all( + 4.0, + ), + child: const Icon( + Icons.keyboard_arrow_down_rounded, + size: 20, + color: Colors.white, + ), + ), + ], + ), + ), + ], ), - textAlign: TextAlign.center, ), - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(L10n.of(context).close), - ), - if (widget.summaryStateEventId != null && - widget.analyticsRoomId != null) - const SizedBox(width: 16), - if (widget.summaryStateEventId != null && - widget.analyticsRoomId != null) - // Show summary button - ElevatedButton( - onPressed: () { - showDialog( - context: context, - builder: (context) => LevelSummaryDialog( - level: widget.level, - analyticsRoomId: widget.analyticsRoomId!, - summaryStateEventId: widget.summaryStateEventId!, - constructSummary: widget.constructSummary, + AnimatedSize( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: _showDetails + ? Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.5, + maxHeight: + MediaQuery.of(context).size.height * 0.75, ), - ); - }, - child: Text(L10n.of(context).levelSummaryTrigger), - ), - ], - ), - ], + margin: const EdgeInsets.only( + top: 16, + ), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.all(16), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 24.0, + children: [ + Table( + columnWidths: const { + 0: IntrinsicColumnWidth(), + 1: FlexColumnWidth(), + 2: IntrinsicColumnWidth(), + }, + defaultVerticalAlignment: + TableCellVerticalAlignment.middle, + children: [ + ...LearningSkillsEnum.values + .where( + (v) => + v.isVisible && _skillsPoints(v) > -1, + ) + .map((skill) { + return TableRow( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 9.0, + horizontal: 18.0, + ), + child: Icon( + skill.icon, + size: 25, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 9.0, + horizontal: 18.0, + ), + child: Text( + skill.tooltip(context), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + textAlign: TextAlign.center, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 9.0, + horizontal: 18.0, + ), + child: Text( + "+ ${_skillsPoints(skill)} XP", + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + textAlign: TextAlign.center, + ), + ), + ], + ); + }), + ], + ), + CachedNetworkImage( + imageUrl: + "${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}", + width: 400, + fit: BoxFit.cover, + ), + if (widget.constructSummary?.textSummary != + null) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .secondaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + widget.constructSummary!.textSummary, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox( + height: 24, + ), + // Share button, currently no functionality + // ElevatedButton( + // onPressed: () { + // // Add share functionality + // }, + // style: ElevatedButton.styleFrom( + // backgroundColor: Colors.white, + // foregroundColor: Colors.black, + // padding: const EdgeInsets.symmetric( + // vertical: 12, + // horizontal: 24, + // ), + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(8), + // ), + // ), + // child: const Row( + // mainAxisSize: MainAxisSize + // .min, + // children: [ + // Text( + // "Share with Friends", + // style: TextStyle( + // fontSize: 16, + // fontWeight: FontWeight.bold, + // ), + // ), + // SizedBox( + // width: 8, + // ), + // Icon( + // Icons.ios_share, + // size: 20, + // ), + // ), + // ), + // ), + ], + ), + ), + ) + : const SizedBox.shrink(), + ), + ], + ), ), - ], - ), + ), + ], ); } } diff --git a/lib/pangea/chat/utils/unlocked_morphs_snackbar.dart b/lib/pangea/chat/utils/unlocked_morphs_snackbar.dart index 675676ef6..ae384d6b2 100644 --- a/lib/pangea/chat/utils/unlocked_morphs_snackbar.dart +++ b/lib/pangea/chat/utils/unlocked_morphs_snackbar.dart @@ -34,7 +34,11 @@ class ConstructNotificationUtil { } static void onClose(ConstructIdentifier construct) { - MatrixState.pAnyState.closeOverlay("${construct.string}_snackbar"); + final overlayKey = "${construct.string}_snackbar"; + MatrixState.pAnyState.closeOverlay(overlayKey); + + MatrixState.pAnyState.activeOverlays.remove(overlayKey); + unlockedConstructs.remove(construct); closeCompleter?.complete(); closeCompleter = null; @@ -66,8 +70,13 @@ class ConstructNotificationUtil { canPop: false, ); + MatrixState.pAnyState.activeOverlays + .add("${construct.string}_snackbar"); + await closeCompleter!.future; } catch (e) { + MatrixState.pAnyState.activeOverlays + .remove("${construct.string}_snackbar"); showingNotification = false; break; } diff --git a/lib/pangea/choreographer/widgets/igc/message_analytics_feedback.dart b/lib/pangea/choreographer/widgets/igc/message_analytics_feedback.dart index dafde919d..77f0b4be7 100644 --- a/lib/pangea/choreographer/widgets/igc/message_analytics_feedback.dart +++ b/lib/pangea/choreographer/widgets/igc/message_analytics_feedback.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:material_symbols_icons/symbols.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart'; @@ -213,8 +212,8 @@ class NewConstructsBadge extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Icon( - Symbols.toys_and_games, - color: ProgressIndicatorEnum.morphsUsed.color(context), + type.indicator.icon, + color: type.indicator.color(context), size: 24, ), const SizedBox(width: 4.0), @@ -223,7 +222,7 @@ class NewConstructsBadge extends StatelessWidget { endValue: newConstructs, startAnimation: opacityAnimation.value > 0.9, style: TextStyle( - color: ProgressIndicatorEnum.morphsUsed.color(context), + color: type.indicator.color(context), fontWeight: FontWeight.bold, ), ), diff --git a/lib/pangea/common/utils/any_state_holder.dart b/lib/pangea/common/utils/any_state_holder.dart index e759159fa..da2ca0e3e 100644 --- a/lib/pangea/common/utils/any_state_holder.dart +++ b/lib/pangea/common/utils/any_state_holder.dart @@ -18,6 +18,7 @@ class OverlayListEntry { } class PangeaAnyState { + final Set activeOverlays = {}; final Map _layerLinkAndKeys = {}; List entries = []; @@ -56,6 +57,11 @@ class PangeaAnyState { canPop: canPop, ), ); + + if (overlayKey != null) { + activeOverlays.add(overlayKey); + } + Overlay.of(context).insert(entry); } @@ -79,6 +85,10 @@ class PangeaAnyState { ); } entries.remove(entry); + + if (overlayKey != null) { + activeOverlays.remove(overlayKey); + } } } @@ -92,6 +102,7 @@ class PangeaAnyState { .toList(); } if (shouldRemove.isEmpty) return; + for (int i = 0; i < shouldRemove.length; i++) { try { shouldRemove[i].entry.remove(); @@ -104,6 +115,11 @@ class PangeaAnyState { }, ); } + + if (shouldRemove[i].key != null) { + activeOverlays.remove(shouldRemove[i].key); + } + entries.remove(shouldRemove[i]); } } diff --git a/lib/pangea/constructs/construct_repo.dart b/lib/pangea/constructs/construct_repo.dart index 38adf6a6e..6d97ce0af 100644 --- a/lib/pangea/constructs/construct_repo.dart +++ b/lib/pangea/constructs/construct_repo.dart @@ -13,12 +13,20 @@ class ConstructSummary { final int lowerLevel; final String language; final String textSummary; + final int writingConstructScore; + final int readingConstructScore; + final int hearingConstructScore; + final int speakingConstructScore; ConstructSummary({ required this.upperLevel, required this.lowerLevel, required this.language, required this.textSummary, + required this.writingConstructScore, + required this.readingConstructScore, + required this.hearingConstructScore, + required this.speakingConstructScore, }); Map toJson() { @@ -27,6 +35,10 @@ class ConstructSummary { 'lower_level': lowerLevel, 'language': language, 'text_summary': textSummary, + 'writing_construct_score': writingConstructScore, + 'reading_construct_score': readingConstructScore, + 'hearing_construct_score': hearingConstructScore, + 'speaking_construct_score': speakingConstructScore, }; } @@ -36,6 +48,10 @@ class ConstructSummary { lowerLevel: json['lower_level'], language: json['language'], textSummary: json['text_summary'], + writingConstructScore: json['writing_construct_score'], + readingConstructScore: json['reading_construct_score'], + hearingConstructScore: json['hearing_construct_score'], + speakingConstructScore: json['speaking_construct_score'], ); } } @@ -99,16 +115,6 @@ class ConstructSummaryResponse { } } -class GenerateConstructSummaryResult { - final String stateEventId; - final ConstructSummary summary; - - GenerateConstructSummaryResult({ - required this.stateEventId, - required this.summary, - }); -} - class ConstructRepo { static Future generateConstructSummary( ConstructSummaryRequest request,