From c13a316a31d38c32fc466d3a9458c36a32a28695 Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Wed, 4 Jun 2025 16:15:13 -0400 Subject: [PATCH 01/32] feat: change UI of level up slightly and add animated bar before details summary WIP: add a level up bar animation that plays before details screen when level up banner is clicked on. Also changed banner icon and background color --- lib/pangea/analytics_misc/level_up.dart | 397 ++++++++++++++---------- 1 file changed, 240 insertions(+), 157 deletions(-) diff --git a/lib/pangea/analytics_misc/level_up.dart b/lib/pangea/analytics_misc/level_up.dart index 3325089e4..872e8f450 100644 --- a/lib/pangea/analytics_misc/level_up.dart +++ b/lib/pangea/analytics_misc/level_up.dart @@ -1,19 +1,19 @@ import 'dart:async'; -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:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart'; +import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar.dart'; +import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_details.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/common/utils/overlay.dart'; import 'package:fluffychat/pangea/constructs/construct_repo.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; class LevelUpConstants { static const String starFileName = "star.png"; @@ -88,6 +88,7 @@ class LevelUpBannerState extends State bool _showDetails = false; bool _showedDetails = false; + bool _showingLevelingAnimation = false; ConstructSummary? _constructSummary; String? _error; @@ -177,9 +178,15 @@ class LevelUpBannerState extends State if (!Environment.isStagingEnvironment) return; if (mounted) { + if (!_showedDetails) { + setState(() { + _showingLevelingAnimation = true; + }); + } + setState(() { _showDetails = !_showDetails; - if (_showDetails && !_showedDetails) { + if (_showDetails && _showedDetails) { _showedDetails = true; } }); @@ -188,6 +195,14 @@ class LevelUpBannerState extends State ? _sizeController.forward() : _sizeController.reverse()); + if (_showDetails && _showingLevelingAnimation) { + await Future.delayed(const Duration(seconds: 2)); + if (!mounted) return; + setState(() { + _showingLevelingAnimation = false; + }); + } + if (!_showDetails) { await Future.delayed( const Duration(milliseconds: 300), @@ -240,9 +255,10 @@ class LevelUpBannerState extends State top: 16, ), decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .secondaryContainer, + color: + Theme.of(context).brightness == Brightness.dark + ? Colors.black + : Colors.white, borderRadius: BorderRadius.circular(8), ), padding: const EdgeInsets.symmetric( @@ -258,10 +274,8 @@ class LevelUpBannerState extends State text: TextSpan( children: [ TextSpan( - text: L10n.of(context) - .congratulationsOnReaching( - widget.level, - ), + //Hardcoded for now, put in translations later + text: "Level up", style: style, ), TextSpan( @@ -287,7 +301,7 @@ class LevelUpBannerState extends State duration: FluffyThemes.animationDuration, child: _error == null ? FluffyThemes.isColumnMode(context) - ? ElevatedButton( + ? IconButton( style: IconButton.styleFrom( padding: const EdgeInsets .symmetric( @@ -296,8 +310,11 @@ class LevelUpBannerState extends State ), ), onPressed: _toggleDetails, - child: Text( - L10n.of(context).details, + icon: Icon( + Icons.arrow_drop_down, + color: Theme.of(context) + .colorScheme + .onSurface, ), ) : SizedBox( @@ -337,10 +354,6 @@ class LevelUpBannerState extends State ], ), ), - IconButton( - icon: const Icon(Icons.close), - onPressed: _close, - ), ], ), ], @@ -350,156 +363,168 @@ class LevelUpBannerState extends State SizeTransition( sizeFactor: _sizeAnimation, child: Container( - constraints: BoxConstraints( - maxHeight: - MediaQuery.of(context).size.height * 0.75, - ), + height: MediaQuery.of(context).size.height * 0.75, + width: MediaQuery.of(context).size.width * .5, margin: const EdgeInsets.only( top: 4.0, ), decoration: BoxDecoration( - color: Colors.black, + color: + Theme.of(context).brightness == Brightness.dark + ? Colors.black + : Colors.white, 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( + child: _showingLevelingAnimation + ? const Expanded( + child: LevelUpBarAnimation(), + ) + : SingleChildScrollView( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.center, + spacing: 24.0, + children: [ + Table( + columnWidths: const { + 0: IntrinsicColumnWidth(), + 1: FlexColumnWidth(), + 2: IntrinsicColumnWidth(), + }, + defaultVerticalAlignment: + TableCellVerticalAlignment.middle, children: [ - Padding( - padding: const EdgeInsets.symmetric( - vertical: 9.0, - horizontal: 18.0, - ), - child: Icon( - skill.icon, - size: 25, - color: Colors.white, - ), - ), - 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, - ), - ), + ...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, + color: Theme.of(context) + .colorScheme + .onSurface, + ), + ), + Padding( + padding: const EdgeInsets + .symmetric( + vertical: 9.0, + horizontal: 18.0, + ), + child: Text( + skill.tooltip(context), + style: const TextStyle( + fontSize: 16, + fontWeight: + FontWeight.w600, + ), + 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, + ), + textAlign: TextAlign.center, + ), + ), + ], + ); + }), ], - ); - }), - ], - ), - CachedNetworkImage( - imageUrl: - "${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}", - width: 400, - fit: BoxFit.cover, - ), - if (_constructSummary?.textSummary != null) - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .secondaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - _constructSummary!.textSummary, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Theme.of(context) - .colorScheme - .onSecondaryContainer, ), - textAlign: TextAlign.center, - ), + CachedNetworkImage( + imageUrl: + "${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}", + width: 400, + fit: BoxFit.cover, + ), + if (_constructSummary?.textSummary != + null) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: + BorderRadius.circular(8), + ), + child: Text( + _constructSummary!.textSummary, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + 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( - 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, - // ), - // ), - // ), - // ), - ], - ), - ), ), ), ], @@ -513,3 +538,61 @@ class LevelUpBannerState extends State ); } } + +//animated progress bar -- move to own file later +class LevelUpBarAnimation extends StatefulWidget { + const LevelUpBarAnimation({super.key}); + + @override + State createState() => _LevelUpBarAnimationState(); +} + +class _LevelUpBarAnimationState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + + _controller = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + ); + + _animation = Tween(begin: 0, end: 1).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeInOut), + ); + + _controller.forward(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + builder: (context, _) { + return Padding( + padding: const EdgeInsets.all(24), + child: ProgressBar( + levelBars: [ + LevelBarDetails( + widthMultiplier: _animation.value, + currentPoints: 0, + fillColor: AppConfig.goldLight, + ), + ], + height: 20, + ), + ); + }, + ); + } +} From 09bf9fcac227c2341b96476c0669cf4b8bfab6cd Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Thu, 5 Jun 2025 12:27:52 -0400 Subject: [PATCH 02/32] add grammar and vocab animations and user icon --- .../settings_chat/settings_chat_view.dart | 20 ++- lib/pangea/analytics_misc/level_up.dart | 119 +++++++++++++++--- 2 files changed, 117 insertions(+), 22 deletions(-) diff --git a/lib/pages/settings_chat/settings_chat_view.dart b/lib/pages/settings_chat/settings_chat_view.dart index fa165ecfd..74b163759 100644 --- a/lib/pages/settings_chat/settings_chat_view.dart +++ b/lib/pages/settings_chat/settings_chat_view.dart @@ -1,13 +1,14 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/settings_switch_list_tile.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +//import for testing level up +import '../../pangea/analytics_misc/level_up.dart'; import 'settings_chat.dart'; class SettingsChatView extends StatelessWidget { @@ -29,6 +30,17 @@ class SettingsChatView extends StatelessWidget { child: MaxWidthBody( child: Column( children: [ + ElevatedButton( + // Test button for leveling up + onPressed: () { + LevelUpUtil.showLevelUpDialog( + 5, + 4, + context, + ); + }, + child: const Text("Test Level Up Dialog"), + ), // #Pangea // SettingsSwitchListTile.adaptive( // title: L10n.of(context).formattedMessages, diff --git a/lib/pangea/analytics_misc/level_up.dart b/lib/pangea/analytics_misc/level_up.dart index 872e8f450..3368daac1 100644 --- a/lib/pangea/analytics_misc/level_up.dart +++ b/lib/pangea/analytics_misc/level_up.dart @@ -5,6 +5,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; +import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart'; import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart'; import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar.dart'; import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_details.dart'; @@ -14,6 +15,7 @@ import 'package:fluffychat/pangea/constructs/construct_repo.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:material_symbols_icons/symbols.dart'; class LevelUpConstants { static const String starFileName = "star.png"; @@ -550,7 +552,16 @@ class LevelUpBarAnimation extends StatefulWidget { class _LevelUpBarAnimationState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; - late Animation _animation; + late Animation _progressAnimation; + late Animation _newVocab; + late Animation _newGrammar; + + final int startGrammar = 23; + final int endGrammar = 78; + final int startVocab = 54; + final int endVocab = 64; + + //add vocab and grammar animation controllers, then display their values in the text fields below. Easy! @override void initState() { @@ -561,7 +572,15 @@ class _LevelUpBarAnimationState extends State vsync: this, ); - _animation = Tween(begin: 0, end: 1).animate( + _progressAnimation = Tween(begin: 0, end: 1).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeInOut), + ); + + _newVocab = IntTween(begin: startVocab, end: endVocab).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeOut), + ); + + _newGrammar = IntTween(begin: startGrammar, end: endGrammar).animate( CurvedAnimation(parent: _controller, curve: Curves.easeInOut), ); @@ -576,23 +595,87 @@ class _LevelUpBarAnimationState extends State @override Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _animation, - builder: (context, _) { - return Padding( - padding: const EdgeInsets.all(24), - child: ProgressBar( - levelBars: [ - LevelBarDetails( - widthMultiplier: _animation.value, - currentPoints: 0, - fillColor: AppConfig.goldLight, + final Color grammarVocabColor = Theme.of(context).colorScheme.primary; + final TextStyle grammarVocabText = + TextStyle(color: grammarVocabColor, fontSize: 24); + const TextStyle titleText = + TextStyle(color: AppConfig.goldLight, fontSize: 20); + + return Stack( + alignment: AlignmentDirectional.center, + children: [ + Column( + children: [ + ClipOval( + child: Image.asset( + '../../../assets/favicon.png', + width: 150, // Adjust the size as needed + height: 150, + fit: BoxFit.cover, // Use BoxFit.cover to fill the circle ), - ], - height: 20, - ), - ); - }, + ), + const SizedBox( + height: 20, + ), + const Text( + //Language fix later + "You have reached a new level!", + style: titleText, + ), + AnimatedBuilder( + animation: _progressAnimation, + builder: (context, _) { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + ProgressBar( + levelBars: [ + LevelBarDetails( + widthMultiplier: _progressAnimation.value, + currentPoints: 0, + fillColor: AppConfig.goldLight, + ), + ], + height: 20, + ), + const SizedBox(height: 45), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Symbols.dictionary, + color: grammarVocabColor, + size: 35, + ), + Text( + "${_newVocab.value}", + style: grammarVocabText, + ), + const SizedBox(width: 40), + Icon( + Symbols.toys_and_games, + color: grammarVocabColor, + size: 35, + ), + Text( + "${_newGrammar.value}", + style: grammarVocabText, + ), + ], + ), + ], + ), + ); + }, + ), + ], + ), + const PointsGainedAnimation( + points: 10, + targetID: "targetID?", + ), + ], ); } } From 1a1f4d6ae33a2413532ea21be6c963daa3fb5eb8 Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Fri, 6 Jun 2025 17:01:21 -0400 Subject: [PATCH 03/32] Popup instead of slide, more animations Fade in level summary and shrinking profile picture as well as some minor UI tweaks --- lib/pangea/analytics_misc/level_up.dart | 682 ++++++++++-------------- 1 file changed, 286 insertions(+), 396 deletions(-) diff --git a/lib/pangea/analytics_misc/level_up.dart b/lib/pangea/analytics_misc/level_up.dart index 3368daac1..e11d5f5ae 100644 --- a/lib/pangea/analytics_misc/level_up.dart +++ b/lib/pangea/analytics_misc/level_up.dart @@ -5,16 +5,16 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; -import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart'; import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart'; import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar.dart'; import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_details.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/common/utils/overlay.dart'; +import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart'; import 'package:fluffychat/pangea/constructs/construct_repo.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:material_symbols_icons/symbols.dart'; class LevelUpConstants { @@ -56,6 +56,12 @@ class LevelUpUtil { child: LevelUpBanner( level: level, prevLevel: prevLevel, + backButtonOverride: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + Navigator.of(context).pop(); + }, + ), ), transformTargetId: '', position: OverlayPositionEnum.top, @@ -69,10 +75,12 @@ class LevelUpUtil { class LevelUpBanner extends StatefulWidget { final int level; final int prevLevel; + final Widget? backButtonOverride; const LevelUpBanner({ required this.level, required this.prevLevel, + required this.backButtonOverride, super.key, }); @@ -86,11 +94,8 @@ class LevelUpBannerState extends State late Animation _slideAnimation; late AnimationController _sizeController; - late Animation _sizeAnimation; - bool _showDetails = false; - bool _showedDetails = false; - bool _showingLevelingAnimation = false; + final bool _showedDetails = false; ConstructSummary? _constructSummary; String? _error; @@ -98,7 +103,6 @@ class LevelUpBannerState extends State @override void initState() { super.initState(); - _setConstructSummary(); _slideController = AnimationController( vsync: this, @@ -120,23 +124,18 @@ class LevelUpBannerState extends State duration: FluffyThemes.animationDuration, ); - _sizeAnimation = Tween( - begin: 0, - end: 1, - ).animate( - CurvedAnimation( - parent: _sizeController, - curve: Curves.easeOut, - ), - ); - _slideController.forward(); - Future.delayed(const Duration(seconds: 15), () async { + Future.delayed(const Duration(seconds: 10), () async { if (mounted && !_showedDetails) _close(); }); } + Future _close() async { + await _slideController.reverse(); + MatrixState.pAnyState.closeOverlay("level_up_notification"); + } + @override void dispose() { _slideController.dispose(); @@ -144,77 +143,38 @@ class LevelUpBannerState extends State super.dispose(); } - Future _setConstructSummary() async { - try { - _constructSummary = await MatrixState.pangeaController.getAnalytics - .generateLevelUpAnalytics( - widget.level, - widget.prevLevel, - ); - } catch (e) { - _error = e.toString(); - } - } - - Future _close() async { - await _slideController.reverse(); - MatrixState.pAnyState.closeOverlay("level_up_notification"); - } - - int _skillsPoints(LearningSkillsEnum skill) { - switch (skill) { - case LearningSkillsEnum.writing: - return _constructSummary?.writingConstructScore ?? 0; - case LearningSkillsEnum.reading: - return _constructSummary?.readingConstructScore ?? 0; - case LearningSkillsEnum.speaking: - return _constructSummary?.speakingConstructScore ?? 0; - case LearningSkillsEnum.hearing: - return _constructSummary?.hearingConstructScore ?? 0; - default: - return 0; - } - } - Future _toggleDetails() async { if (!Environment.isStagingEnvironment) return; - if (mounted) { - if (!_showedDetails) { - setState(() { - _showingLevelingAnimation = true; - }); - } + await _close(); - setState(() { - _showDetails = !_showDetails; - if (_showDetails && _showedDetails) { - _showedDetails = true; - } - }); + //if (!mounted) return; - await (_showDetails - ? _sizeController.forward() - : _sizeController.reverse()); - - if (_showDetails && _showingLevelingAnimation) { - await Future.delayed(const Duration(seconds: 2)); - if (!mounted) return; - setState(() { - _showingLevelingAnimation = false; - }); - } - - if (!_showDetails) { - await Future.delayed( - const Duration(milliseconds: 300), - () async { - if (!mounted) return; - _close(); - }, - ); - } - } + await showDialog( + context: context, + builder: (context) => FullWidthDialog( + maxWidth: 400, + maxHeight: 800, + dialogContent: Scaffold( + appBar: AppBar( + centerTitle: true, + title: kIsWeb + ? const Text( + "You have leveled up!", + style: TextStyle( + color: AppConfig.gold, + fontWeight: FontWeight.w600, + ), + ) + : null, + ), + body: LevelUpBarAnimation( + prevLevel: widget.prevLevel, + level: widget.level, + ), + ), + ), + ); } @override @@ -257,11 +217,11 @@ class LevelUpBannerState extends State top: 16, ), decoration: BoxDecoration( - color: - Theme.of(context).brightness == Brightness.dark - ? Colors.black - : Colors.white, + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(8), + border: Border.all( + color: AppConfig.gold, + ), ), padding: const EdgeInsets.symmetric( vertical: 16, @@ -301,60 +261,21 @@ class LevelUpBannerState extends State if (Environment.isStagingEnvironment) AnimatedSize( duration: FluffyThemes.animationDuration, - child: _error == null - ? FluffyThemes.isColumnMode(context) - ? IconButton( - style: IconButton.styleFrom( - padding: const EdgeInsets - .symmetric( - vertical: 4.0, - horizontal: 16.0, - ), - ), - onPressed: _toggleDetails, - icon: Icon( - Icons.arrow_drop_down, - color: Theme.of(context) - .colorScheme - .onSurface, - ), - ) - : SizedBox( - width: 32.0, - height: 32.0, - child: Center( - child: IconButton( - icon: const Icon( - Icons.info_outline, - ), - style: - IconButton.styleFrom( - padding: - const EdgeInsets - .all( - 4.0, - ), - ), - onPressed: _toggleDetails, - constraints: - const BoxConstraints(), - ), - ), - ) - : Row( - children: [ - Tooltip( - message: L10n.of(context) - .oopsSomethingWentWrong, - child: Icon( - Icons.error, - color: Theme.of(context) - .colorScheme - .error, - ), - ), - ], - ), + child: IconButton( + style: IconButton.styleFrom( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 16.0, + ), + ), + onPressed: _toggleDetails, + icon: Icon( + Icons.arrow_drop_down, + color: Theme.of(context) + .colorScheme + .onSurface, + ), + ), ), ], ), @@ -362,173 +283,6 @@ class LevelUpBannerState extends State ), ), ), - SizeTransition( - sizeFactor: _sizeAnimation, - child: Container( - height: MediaQuery.of(context).size.height * 0.75, - width: MediaQuery.of(context).size.width * .5, - margin: const EdgeInsets.only( - top: 4.0, - ), - decoration: BoxDecoration( - color: - Theme.of(context).brightness == Brightness.dark - ? Colors.black - : Colors.white, - borderRadius: BorderRadius.circular(8), - ), - padding: const EdgeInsets.all(16), - child: _showingLevelingAnimation - ? const Expanded( - child: LevelUpBarAnimation(), - ) - : 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, - color: Theme.of(context) - .colorScheme - .onSurface, - ), - ), - Padding( - padding: const EdgeInsets - .symmetric( - vertical: 9.0, - horizontal: 18.0, - ), - child: Text( - skill.tooltip(context), - style: const TextStyle( - fontSize: 16, - fontWeight: - FontWeight.w600, - ), - 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, - ), - textAlign: TextAlign.center, - ), - ), - ], - ); - }), - ], - ), - CachedNetworkImage( - imageUrl: - "${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}", - width: 400, - fit: BoxFit.cover, - ), - if (_constructSummary?.textSummary != - null) - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: - BorderRadius.circular(8), - ), - child: Text( - _constructSummary!.textSummary, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: Theme.of(context) - .colorScheme - .onSecondaryContainer, - ), - 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, - // ), - // ), - // ), - // ), - ], - ), - ), - ), - ), ], ), ), @@ -543,7 +297,14 @@ class LevelUpBannerState extends State //animated progress bar -- move to own file later class LevelUpBarAnimation extends StatefulWidget { - const LevelUpBarAnimation({super.key}); + final int prevLevel; + final int level; + + const LevelUpBarAnimation({ + super.key, + required this.prevLevel, + required this.level, + }); @override State createState() => _LevelUpBarAnimationState(); @@ -551,42 +312,92 @@ class LevelUpBarAnimation extends StatefulWidget { class _LevelUpBarAnimationState extends State with SingleTickerProviderStateMixin { - late AnimationController _controller; - late Animation _progressAnimation; - late Animation _newVocab; - late Animation _newGrammar; + late final AnimationController _controller; + late final Animation _progressAnimation; + late final Animation _vocabAnimation; + late final Animation _grammarAnimation; + late final Animation _skillsOpacity; + late final Animation _shrinkMultiplier; - final int startGrammar = 23; - final int endGrammar = 78; - final int startVocab = 54; - final int endVocab = 64; + ConstructSummary? _constructSummary; - //add vocab and grammar animation controllers, then display their values in the text fields below. Easy! + static const int _startGrammar = 23; + static const int _endGrammar = 78; + static const int _startVocab = 54; + static const int _endVocab = 64; + + static const double _startOpacity = 0.0; + static const double _endOpacity = 1.0; + static const Duration _animationDuration = Duration(seconds: 3); @override void initState() { super.initState(); + _loadConstructSummary(); + _controller = AnimationController( - duration: const Duration(milliseconds: 1500), + duration: _animationDuration, vsync: this, ); _progressAnimation = Tween(begin: 0, end: 1).animate( - CurvedAnimation(parent: _controller, curve: Curves.easeInOut), + CurvedAnimation(parent: _controller, curve: Curves.easeOutBack), ); - _newVocab = IntTween(begin: startVocab, end: endVocab).animate( - CurvedAnimation(parent: _controller, curve: Curves.easeOut), + _vocabAnimation = IntTween(begin: _startVocab, end: _endVocab).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.5, curve: Curves.easeInOutQuad), + ), ); - _newGrammar = IntTween(begin: startGrammar, end: endGrammar).animate( - CurvedAnimation(parent: _controller, curve: Curves.easeInOut), + _grammarAnimation = + IntTween(begin: _startGrammar, end: _endGrammar).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.5, curve: Curves.easeInOutQuad), + ), + ); + + _skillsOpacity = + Tween(begin: _startOpacity, end: _endOpacity).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.5, 1.0, curve: Curves.easeIn), + ), + ); + + _shrinkMultiplier = Tween(begin: 1.0, end: 0.5).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.5, 1.0, curve: Curves.easeInOut), + ), ); _controller.forward(); } + Future _loadConstructSummary() async { + final summary = await MatrixState.pangeaController.getAnalytics + .generateLevelUpAnalytics(widget.level, widget.prevLevel); + setState(() => _constructSummary = summary); + } + + int _getSkillXP(LearningSkillsEnum skill) { + return switch (skill) { + LearningSkillsEnum.writing => + _constructSummary?.writingConstructScore ?? 0, + LearningSkillsEnum.reading => + _constructSummary?.readingConstructScore ?? 0, + LearningSkillsEnum.speaking => + _constructSummary?.speakingConstructScore ?? 0, + LearningSkillsEnum.hearing => + _constructSummary?.hearingConstructScore ?? 0, + _ => 0, + }; + } + @override void dispose() { _controller.dispose(); @@ -595,87 +406,166 @@ class _LevelUpBarAnimationState extends State @override Widget build(BuildContext context) { - final Color grammarVocabColor = Theme.of(context).colorScheme.primary; - final TextStyle grammarVocabText = - TextStyle(color: grammarVocabColor, fontSize: 24); - const TextStyle titleText = - TextStyle(color: AppConfig.goldLight, fontSize: 20); + final colorScheme = Theme.of(context).colorScheme; + final grammarVocabStyle = + TextStyle(color: colorScheme.primary, fontSize: 24); - return Stack( - alignment: AlignmentDirectional.center, + return Column( children: [ - Column( - children: [ - ClipOval( - child: Image.asset( - '../../../assets/favicon.png', - width: 150, // Adjust the size as needed - height: 150, - fit: BoxFit.cover, // Use BoxFit.cover to fill the circle + AnimatedBuilder( + animation: _progressAnimation, + builder: (_, __) => Column( + children: [ + // Avatar (static size) + ClipOval( + child: Image.asset( + '../../../assets/favicon.png', + width: 150 * _shrinkMultiplier.value, + height: 150 * _shrinkMultiplier.value, + fit: BoxFit.cover, + ), ), - ), - const SizedBox( - height: 20, - ), - const Text( - //Language fix later - "You have reached a new level!", - style: titleText, - ), - AnimatedBuilder( - animation: _progressAnimation, - builder: (context, _) { - return Padding( - padding: const EdgeInsets.all(24), - child: Column( - children: [ - ProgressBar( - levelBars: [ - LevelBarDetails( - widthMultiplier: _progressAnimation.value, - currentPoints: 0, - fillColor: AppConfig.goldLight, - ), - ], - height: 20, + SizedBox(height: 10 * _shrinkMultiplier.value), + ], + ), + ), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(15), + child: Column( + children: [ + // Animated progress bar + AnimatedBuilder( + animation: _progressAnimation, + builder: (_, __) => ProgressBar( + levelBars: [ + LevelBarDetails( + widthMultiplier: _progressAnimation.value, + currentPoints: 0, + fillColor: AppConfig.goldLight, ), - const SizedBox(height: 45), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Symbols.dictionary, - color: grammarVocabColor, - size: 35, - ), - Text( - "${_newVocab.value}", - style: grammarVocabText, - ), - const SizedBox(width: 40), - Icon( - Symbols.toys_and_games, - color: grammarVocabColor, - size: 35, - ), - Text( - "${_newGrammar.value}", - style: grammarVocabText, - ), - ], + ], + height: 20, + ), + ), + const SizedBox(height: 25), + // Animated vocab and grammar row + AnimatedBuilder( + animation: _controller, + builder: (_, __) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Symbols.dictionary, + color: colorScheme.primary, + size: 35, + ), + Text( + '${_vocabAnimation.value}', + style: grammarVocabStyle, + ), + const SizedBox(width: 40), + Icon( + Symbols.toys_and_games, + color: colorScheme.primary, + size: 35, + ), + Text( + '${_grammarAnimation.value}', + style: grammarVocabStyle, ), ], ), - ); - }, + ), + const SizedBox(height: 32), + // Skills section (fades in) + AnimatedBuilder( + animation: _skillsOpacity, + builder: (_, __) => Opacity( + opacity: _skillsOpacity.value, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildSkillsTable(context), + const SizedBox(height: 24), + if (_constructSummary?.textSummary != null) + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text( + _constructSummary!.textSummary, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: colorScheme.onSecondaryContainer, + ), + ), + ), + CachedNetworkImage( + imageUrl: + "${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}", + width: 400, + fit: BoxFit.cover, + ), + ], + ), + ), + ), + ], ), - ], - ), - const PointsGainedAnimation( - points: 10, - targetID: "targetID?", + ), ), ], ); } + + Widget _buildSkillsTable(BuildContext context) { + final visibleSkills = LearningSkillsEnum.values.where( + (skill) => skill.isVisible && _getSkillXP(skill) > -1, + ); + + return Table( + columnWidths: const { + 0: IntrinsicColumnWidth(), + 1: FlexColumnWidth(), + 2: IntrinsicColumnWidth(), + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: visibleSkills.map((skill) { + return TableRow( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(vertical: 9.0, horizontal: 18.0), + child: Icon( + skill.icon, + size: 25, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(vertical: 9.0, horizontal: 18.0), + child: Text( + skill.tooltip(context), + style: + const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + textAlign: TextAlign.center, + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(vertical: 9.0, horizontal: 18.0), + child: Text( + "+ ${_getSkillXP(skill)} XP", + style: + const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + textAlign: TextAlign.center, + ), + ), + ], + ); + }).toList(), + ); + } } From dcd9699de810a5ff03de7f6ccc94339488272a59 Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Mon, 9 Jun 2025 16:55:53 -0400 Subject: [PATCH 04/32] Animations + confetti Added a few tweaks to the animations and added the confetti package --- lib/pangea/analytics_misc/level_up.dart | 385 +++++++++++++++--------- pubspec.yaml | 1 + 2 files changed, 238 insertions(+), 148 deletions(-) diff --git a/lib/pangea/analytics_misc/level_up.dart b/lib/pangea/analytics_misc/level_up.dart index e11d5f5ae..d7ee60af5 100644 --- a/lib/pangea/analytics_misc/level_up.dart +++ b/lib/pangea/analytics_misc/level_up.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:audioplayers/audioplayers.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:confetti/confetti.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; @@ -319,21 +320,29 @@ class _LevelUpBarAnimationState extends State late final Animation _skillsOpacity; late final Animation _shrinkMultiplier; + late final ConfettiController _confettiController; + ConstructSummary? _constructSummary; + int displayedLevel = -1; + bool _hasBlastedConfetti = false; + static const int _startGrammar = 23; static const int _endGrammar = 78; static const int _startVocab = 54; static const int _endVocab = 64; + static const String language = "ES"; - static const double _startOpacity = 0.0; - static const double _endOpacity = 1.0; - static const Duration _animationDuration = Duration(seconds: 3); + static const Duration _animationDuration = Duration(seconds: 6); @override void initState() { super.initState(); + displayedLevel = widget.prevLevel; + _confettiController = + ConfettiController(duration: const Duration(seconds: 1)); + _loadConstructSummary(); _controller = AnimationController( @@ -341,6 +350,22 @@ class _LevelUpBarAnimationState extends State vsync: this, ); + // halfway through the animation, switch to the new level + _controller.addListener(() { + if (_controller.value >= 0.5 && displayedLevel == widget.prevLevel) { + setState(() { + displayedLevel = widget.level; + }); + } + }); + + _controller.addListener(() { + if (_controller.value >= 0.5 && !_hasBlastedConfetti) { + _confettiController.play(); + _hasBlastedConfetti = true; + } + }); + _progressAnimation = Tween(begin: 0, end: 1).animate( CurvedAnimation(parent: _controller, curve: Curves.easeOutBack), ); @@ -360,18 +385,17 @@ class _LevelUpBarAnimationState extends State ), ); - _skillsOpacity = - Tween(begin: _startOpacity, end: _endOpacity).animate( + _skillsOpacity = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _controller, - curve: const Interval(0.5, 1.0, curve: Curves.easeIn), + curve: const Interval(0.7, 1.0, curve: Curves.easeIn), ), ); _shrinkMultiplier = Tween(begin: 1.0, end: 0.5).animate( CurvedAnimation( parent: _controller, - curve: const Interval(0.5, 1.0, curve: Curves.easeInOut), + curve: const Interval(0.7, 1.0, curve: Curves.easeInOut), ), ); @@ -401,118 +425,160 @@ class _LevelUpBarAnimationState extends State @override void dispose() { _controller.dispose(); + _confettiController.dispose(); super.dispose(); } + @override @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - final grammarVocabStyle = - TextStyle(color: colorScheme.primary, fontSize: 24); + final grammarVocabStyle = Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.primary, + ); - return Column( + return Stack( children: [ - AnimatedBuilder( - animation: _progressAnimation, - builder: (_, __) => Column( + SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - // Avatar (static size) - ClipOval( - child: Image.asset( - '../../../assets/favicon.png', - width: 150 * _shrinkMultiplier.value, - height: 150 * _shrinkMultiplier.value, - fit: BoxFit.cover, + // Avatar and language + AnimatedBuilder( + animation: _progressAnimation, + builder: (_, __) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ClipOval( + child: Image.asset( + './assets/favicon.png', + width: 150 * _shrinkMultiplier.value, + height: 150 * _shrinkMultiplier.value, + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 16), + Text( + language, + style: TextStyle( + fontSize: 24 * _skillsOpacity.value, + color: AppConfig.goldLight, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const SizedBox(height: 20), + + // Progress bar + Level + AnimatedBuilder( + animation: _progressAnimation, + builder: (_, __) => Row( + children: [ + Expanded( + child: ProgressBar( + levelBars: [ + LevelBarDetails( + widthMultiplier: _progressAnimation.value, + currentPoints: 0, + fillColor: AppConfig.goldLight, + ), + ], + height: 20, + ), + ), + const SizedBox(width: 8), + Text( + "⭐ $displayedLevel", + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: AppConfig.goldLight, + ), + ), + ], + ), + ), + const SizedBox(height: 25), + + // Vocab and grammar row + AnimatedBuilder( + animation: _controller, + builder: (_, __) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Symbols.dictionary, + color: colorScheme.primary, + size: 35, + ), + const SizedBox(width: 8), + Text('${_vocabAnimation.value}', style: grammarVocabStyle), + const SizedBox(width: 40), + Icon( + Symbols.toys_and_games, + color: colorScheme.primary, + size: 35, + ), + const SizedBox(width: 8), + Text( + '${_grammarAnimation.value}', + style: grammarVocabStyle, + ), + ], + ), + ), + const SizedBox(height: 32), + + // Skills section + AnimatedBuilder( + animation: _skillsOpacity, + builder: (_, __) => Opacity( + opacity: _skillsOpacity.value, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildSkillsTable(context), + const SizedBox(height: 24), + if (_constructSummary?.textSummary != null) + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text( + _constructSummary!.textSummary, + textAlign: TextAlign.left, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + const SizedBox(height: 24), + CachedNetworkImage( + imageUrl: + "${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}", + width: 400, + fit: BoxFit.cover, + ), + ], + ), ), ), - SizedBox(height: 10 * _shrinkMultiplier.value), ], ), ), - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(15), - child: Column( - children: [ - // Animated progress bar - AnimatedBuilder( - animation: _progressAnimation, - builder: (_, __) => ProgressBar( - levelBars: [ - LevelBarDetails( - widthMultiplier: _progressAnimation.value, - currentPoints: 0, - fillColor: AppConfig.goldLight, - ), - ], - height: 20, - ), - ), - const SizedBox(height: 25), - // Animated vocab and grammar row - AnimatedBuilder( - animation: _controller, - builder: (_, __) => Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Symbols.dictionary, - color: colorScheme.primary, - size: 35, - ), - Text( - '${_vocabAnimation.value}', - style: grammarVocabStyle, - ), - const SizedBox(width: 40), - Icon( - Symbols.toys_and_games, - color: colorScheme.primary, - size: 35, - ), - Text( - '${_grammarAnimation.value}', - style: grammarVocabStyle, - ), - ], - ), - ), - const SizedBox(height: 32), - // Skills section (fades in) - AnimatedBuilder( - animation: _skillsOpacity, - builder: (_, __) => Opacity( - opacity: _skillsOpacity.value, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _buildSkillsTable(context), - const SizedBox(height: 24), - if (_constructSummary?.textSummary != null) - Padding( - padding: const EdgeInsets.only(top: 16), - child: Text( - _constructSummary!.textSummary, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: colorScheme.onSecondaryContainer, - ), - ), - ), - CachedNetworkImage( - imageUrl: - "${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}", - width: 400, - fit: BoxFit.cover, - ), - ], - ), - ), - ), - ], - ), + + // Confetti overlay + Align( + alignment: Alignment.topCenter, + child: ConfettiWidget( + confettiController: _confettiController, + blastDirectionality: BlastDirectionality.explosive, + emissionFrequency: 0.2, + numberOfParticles: 30, + gravity: 0.4, + colors: const [ + AppConfig.goldLight, + AppConfig.gold, + ], ), ), ], @@ -520,50 +586,73 @@ class _LevelUpBarAnimationState extends State } Widget _buildSkillsTable(BuildContext context) { - final visibleSkills = LearningSkillsEnum.values.where( - (skill) => skill.isVisible && _getSkillXP(skill) > -1, - ); + final visibleSkills = LearningSkillsEnum.values + .where( + (skill) => skill.isVisible && _getSkillXP(skill) > -1, + ) + .toList(); - return Table( - columnWidths: const { - 0: IntrinsicColumnWidth(), - 1: FlexColumnWidth(), - 2: IntrinsicColumnWidth(), - }, - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: visibleSkills.map((skill) { - return TableRow( - children: [ - Padding( - padding: - const EdgeInsets.symmetric(vertical: 9.0, horizontal: 18.0), - child: Icon( - skill.icon, - size: 25, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - Padding( - padding: - const EdgeInsets.symmetric(vertical: 9.0, horizontal: 18.0), - child: Text( - skill.tooltip(context), - style: - const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), - textAlign: TextAlign.center, - ), - ), - Padding( - padding: - const EdgeInsets.symmetric(vertical: 9.0, horizontal: 18.0), - child: Text( - "+ ${_getSkillXP(skill)} XP", - style: - const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), - textAlign: TextAlign.center, - ), - ), - ], + const int itemsPerRow = 3; + + // Break skills into chunks of 3 + final List> rows = []; + for (var i = 0; i < visibleSkills.length; i += itemsPerRow) { + rows.add( + visibleSkills.sublist( + i, + i + itemsPerRow > visibleSkills.length + ? visibleSkills.length + : i + itemsPerRow, + ), + ); + } + + return Column( + children: rows.map((row) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(3, (index) { + if (index < row.length) { + final skill = row[index]; + return Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + skill.tooltip(context), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Icon( + skill.icon, + size: 30, + color: Theme.of(context).colorScheme.onSurface, + ), + const SizedBox(height: 8), + Text( + "+ ${_getSkillXP(skill)} XP", + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppConfig.gold, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } else { + // Empty spacer to keep spacing consistent + return const Expanded(child: SizedBox()); + } + }), + ), ); }).toList(), ); diff --git a/pubspec.yaml b/pubspec.yaml index b8b738d3d..4eef8be64 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: chewie: ^1.8.1 collection: ^1.18.0 cross_file: ^0.3.4+2 + confetti: ^0.8.0 cupertino_icons: any # #Pangea # desktop_drop: ^0.4.4 From 9ed3b6089348f4a0b0f39880187718d6c01517a2 Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Tue, 10 Jun 2025 13:29:33 -0400 Subject: [PATCH 05/32] star confetti, some UI tweaks and animation fixes --- lib/pangea/analytics_misc/level_up.dart | 265 +++++++++++++++--------- 1 file changed, 170 insertions(+), 95 deletions(-) diff --git a/lib/pangea/analytics_misc/level_up.dart b/lib/pangea/analytics_misc/level_up.dart index d7ee60af5..08259547d 100644 --- a/lib/pangea/analytics_misc/level_up.dart +++ b/lib/pangea/analytics_misc/level_up.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'package:audioplayers/audioplayers.dart'; import 'package:cached_network_image/cached_network_image.dart'; @@ -16,6 +17,7 @@ import 'package:fluffychat/pangea/constructs/construct_repo.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:material_symbols_icons/symbols.dart'; class LevelUpConstants { @@ -98,9 +100,6 @@ class LevelUpBannerState extends State final bool _showedDetails = false; - ConstructSummary? _constructSummary; - String? _error; - @override void initState() { super.initState(); @@ -323,6 +322,7 @@ class _LevelUpBarAnimationState extends State late final ConfettiController _confettiController; ConstructSummary? _constructSummary; + String? _error; int displayedLevel = -1; bool _hasBlastedConfetti = false; @@ -341,9 +341,9 @@ class _LevelUpBarAnimationState extends State displayedLevel = widget.prevLevel; _confettiController = - ConfettiController(duration: const Duration(seconds: 1)); + ConfettiController(duration: const Duration(seconds: 3)); - _loadConstructSummary(); + _setConstructSummary(); _controller = AnimationController( duration: _animationDuration, @@ -360,7 +360,7 @@ class _LevelUpBarAnimationState extends State }); _controller.addListener(() { - if (_controller.value >= 0.5 && !_hasBlastedConfetti) { + if (_controller.value >= 0.4 && !_hasBlastedConfetti) { _confettiController.play(); _hasBlastedConfetti = true; } @@ -392,7 +392,7 @@ class _LevelUpBarAnimationState extends State ), ); - _shrinkMultiplier = Tween(begin: 1.0, end: 0.5).animate( + _shrinkMultiplier = Tween(begin: 1.0, end: 0.3).animate( CurvedAnimation( parent: _controller, curve: const Interval(0.7, 1.0, curve: Curves.easeInOut), @@ -402,10 +402,16 @@ class _LevelUpBarAnimationState extends State _controller.forward(); } - Future _loadConstructSummary() async { - final summary = await MatrixState.pangeaController.getAnalytics - .generateLevelUpAnalytics(widget.level, widget.prevLevel); - setState(() => _constructSummary = summary); + Future _setConstructSummary() async { + try { + _constructSummary = await MatrixState.pangeaController.getAnalytics + .generateLevelUpAnalytics( + widget.level, + widget.prevLevel, + ); + } catch (e) { + _error = e.toString(); + } } int _getSkillXP(LearningSkillsEnum skill) { @@ -454,8 +460,8 @@ class _LevelUpBarAnimationState extends State ClipOval( child: Image.asset( './assets/favicon.png', - width: 150 * _shrinkMultiplier.value, - height: 150 * _shrinkMultiplier.value, + width: 200 * _shrinkMultiplier.value, + height: 200 * _shrinkMultiplier.value, fit: BoxFit.cover, ), ), @@ -471,8 +477,6 @@ class _LevelUpBarAnimationState extends State ], ), ), - const SizedBox(height: 20), - // Progress bar + Level AnimatedBuilder( animation: _progressAnimation, @@ -530,55 +534,109 @@ class _LevelUpBarAnimationState extends State ], ), ), - const SizedBox(height: 32), + const SizedBox(height: 16), // Skills section AnimatedBuilder( animation: _skillsOpacity, builder: (_, __) => Opacity( opacity: _skillsOpacity.value, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _buildSkillsTable(context), - const SizedBox(height: 24), - if (_constructSummary?.textSummary != null) - Padding( - padding: const EdgeInsets.only(top: 16), - child: Text( - _constructSummary!.textSummary, - textAlign: TextAlign.left, - style: Theme.of(context).textTheme.bodyMedium, - ), + child: _error == null + ? Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildSkillsTable(context), + const SizedBox(height: 8), + if (_constructSummary?.textSummary != null) + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text( + _constructSummary!.textSummary, + textAlign: TextAlign.left, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + //const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.all(16.0), + child: CachedNetworkImage( + imageUrl: + "${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}", + width: 400, + fit: BoxFit.cover, + ), + ), + ], + ) + // if error getting construct summary + : Row( + children: [ + Tooltip( + message: L10n.of(context).oopsSomethingWentWrong, + child: Icon( + Icons.error, + color: Theme.of(context).colorScheme.error, + ), + ), + ], ), - const SizedBox(height: 24), - CachedNetworkImage( - imageUrl: - "${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}", - width: 400, - fit: BoxFit.cover, - ), - ], + ), + ), + // 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, + ), + ], ), ), ], ), ), - // Confetti overlay Align( alignment: Alignment.topCenter, child: ConfettiWidget( confettiController: _confettiController, - blastDirectionality: BlastDirectionality.explosive, - emissionFrequency: 0.2, - numberOfParticles: 30, - gravity: 0.4, + blastDirectionality: BlastDirectionality + .explosive, // don't specify a direction, blast randomly + shouldLoop: + true, // start again as soon as the animation is finished + emissionFrequency: 0.1, + numberOfParticles: 7, colors: const [ AppConfig.goldLight, AppConfig.gold, - ], + ], // manually specify the colors to be used + createParticlePath: drawStar, // define a custom shape/path. ), ), ], @@ -587,74 +645,91 @@ class _LevelUpBarAnimationState extends State Widget _buildSkillsTable(BuildContext context) { final visibleSkills = LearningSkillsEnum.values - .where( - (skill) => skill.isVisible && _getSkillXP(skill) > -1, - ) + .where((skill) => _getSkillXP(skill) > -1) .toList(); - const int itemsPerRow = 3; - - // Break skills into chunks of 3 - final List> rows = []; - for (var i = 0; i < visibleSkills.length; i += itemsPerRow) { - rows.add( + const itemsPerRow = 3; + // chunk into rows of up to 3 + final rows = >[ + for (var i = 0; i < visibleSkills.length; i += itemsPerRow) visibleSkills.sublist( i, - i + itemsPerRow > visibleSkills.length - ? visibleSkills.length - : i + itemsPerRow, + min(i + itemsPerRow, visibleSkills.length), ), - ); - } + ]; return Column( children: rows.map((row) { return Padding( - padding: const EdgeInsets.symmetric(vertical: 12.0), + padding: const EdgeInsets.symmetric(vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: List.generate(3, (index) { - if (index < row.length) { - final skill = row[index]; - return Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - skill.tooltip(context), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: row.map((skill) { + return Flexible( + fit: FlexFit.loose, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + skill.tooltip(context), + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, ), - const SizedBox(height: 8), - Icon( - skill.icon, - size: 30, - color: Theme.of(context).colorScheme.onSurface, + textAlign: TextAlign.center, + ), + const SizedBox(height: 4), + Icon( + skill.icon, + size: 25, + color: Theme.of(context).colorScheme.onSurface, + ), + const SizedBox(height: 4), + Text( + '+ ${_getSkillXP(skill)} XP', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppConfig.gold, ), - const SizedBox(height: 8), - Text( - "+ ${_getSkillXP(skill)} XP", - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: AppConfig.gold, - ), - textAlign: TextAlign.center, - ), - ], - ), - ); - } else { - // Empty spacer to keep spacing consistent - return const Expanded(child: SizedBox()); - } - }), + textAlign: TextAlign.center, + ), + ], + ), + ); + }).toList(), ), ); }).toList(), ); } + + Path drawStar(Size size) { + // Method to convert degrees to radians + double degToRad(double deg) => deg * (pi / 180.0); + + const numberOfPoints = 5; + final halfWidth = size.width / 2; + final externalRadius = halfWidth; + final internalRadius = halfWidth / 2.5; + final degreesPerStep = degToRad(360 / numberOfPoints); + final halfDegreesPerStep = degreesPerStep / 2; + final path = Path(); + final fullAngle = degToRad(360); + path.moveTo(size.width, halfWidth); + + for (double step = 0; step < fullAngle; step += degreesPerStep) { + path.lineTo( + halfWidth + externalRadius * cos(step), + halfWidth + externalRadius * sin(step), + ); + path.lineTo( + halfWidth + internalRadius * cos(step + halfDegreesPerStep), + halfWidth + internalRadius * sin(step + halfDegreesPerStep), + ); + } + path.close(); + return path; + } } From eaf13337a5c4d319df0d60910284d7ce2b66769f Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Thu, 12 Jun 2025 09:42:24 -0400 Subject: [PATCH 06/32] added user icon and grammar/vocab stats --- lib/pangea/analytics_misc/level_up.dart | 329 +++++++++++++++--------- 1 file changed, 206 insertions(+), 123 deletions(-) diff --git a/lib/pangea/analytics_misc/level_up.dart b/lib/pangea/analytics_misc/level_up.dart index 08259547d..d4fc548b0 100644 --- a/lib/pangea/analytics_misc/level_up.dart +++ b/lib/pangea/analytics_misc/level_up.dart @@ -7,6 +7,7 @@ import 'package:confetti/confetti.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; +import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart'; import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar.dart'; import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_details.dart'; @@ -15,10 +16,12 @@ import 'package:fluffychat/pangea/common/utils/overlay.dart'; import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart'; import 'package:fluffychat/pangea/constructs/construct_repo.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:material_symbols_icons/symbols.dart'; +import 'package:matrix/matrix_api_lite/generated/model.dart'; class LevelUpConstants { static const String starFileName = "star.png"; @@ -144,151 +147,162 @@ class LevelUpBannerState extends State } Future _toggleDetails() async { - if (!Environment.isStagingEnvironment) return; - await _close(); //if (!mounted) return; await showDialog( context: context, - builder: (context) => FullWidthDialog( - maxWidth: 400, - maxHeight: 800, - dialogContent: Scaffold( - appBar: AppBar( - centerTitle: true, - title: kIsWeb - ? const Text( - "You have leveled up!", - style: TextStyle( - color: AppConfig.gold, - fontWeight: FontWeight.w600, - ), - ) - : null, - ), - body: LevelUpBarAnimation( - prevLevel: widget.prevLevel, - level: widget.level, - ), - ), - ), + builder: (context) => LevelUpPopup(widget: widget), ); } @override Widget build(BuildContext context) { - final style = FluffyThemes.isColumnMode(context) + final isColumnMode = FluffyThemes.isColumnMode(context); + + final style = isColumnMode ? Theme.of(context).textTheme.titleLarge?.copyWith( - color: Theme.of(context).colorScheme.onSecondaryContainer, + color: AppConfig.gold, fontWeight: FontWeight.bold, letterSpacing: 0.5, ) : Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).colorScheme.onSecondaryContainer, + color: AppConfig.gold, fontWeight: FontWeight.bold, letterSpacing: 0.5, ); return SafeArea( child: Material( - color: Colors.transparent, - child: Stack( - children: [ - SlideTransition( - position: _slideAnimation, - child: Align( - alignment: Alignment.topCenter, - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 600.0, + type: MaterialType.transparency, + child: SlideTransition( + position: _slideAnimation, + child: LayoutBuilder( + builder: (context, constraints) { + return GestureDetector( + onPanUpdate: (details) { + if (details.delta.dy < -10) _close(); + }, + onTap: _toggleDetails, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 16.0, + horizontal: 4.0, ), - child: Column( - mainAxisSize: MainAxisSize.min, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border( + bottom: BorderSide( + color: AppConfig.gold.withAlpha(200), + width: 2.0, + ), + ), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(AppConfig.borderRadius), + bottomRight: Radius.circular(AppConfig.borderRadius), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - GestureDetector( - onPanUpdate: (details) { - if (details.delta.dy < -10) _close(); - }, - onTap: _toggleDetails, - child: Container( - margin: const EdgeInsets.only( - top: 16, + // Spacer for symmetry + SizedBox( + width: constraints.maxWidth >= 600 ? 120.0 : 65.0, + ), + // Centered content + Expanded( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: isColumnMode ? 16.0 : 8.0, ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: AppConfig.gold, - ), - ), - padding: const EdgeInsets.symmetric( - vertical: 16, - horizontal: 24, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - spacing: 8.0, + child: Wrap( + spacing: 16.0, + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, children: [ - Flexible( - child: RichText( - text: TextSpan( - children: [ - TextSpan( - //Hardcoded for now, put in translations later - text: "Level up", - style: style, - ), - TextSpan( - text: " ", - style: style, - ), - WidgetSpan( - child: CachedNetworkImage( - imageUrl: - "${AppConfig.assetsBaseURL}/${LevelUpConstants.starFileName}", - height: 24, - width: 24, - ), - ), - ], - ), - ), + Text( + "Level up", + style: style, + overflow: TextOverflow.ellipsis, ), - Row( - children: [ - if (Environment.isStagingEnvironment) - AnimatedSize( - duration: FluffyThemes.animationDuration, - child: IconButton( - style: IconButton.styleFrom( - padding: const EdgeInsets.symmetric( - vertical: 4.0, - horizontal: 16.0, - ), - ), - onPressed: _toggleDetails, - icon: Icon( - Icons.arrow_drop_down, - color: Theme.of(context) - .colorScheme - .onSurface, - ), - ), - ), - ], + CachedNetworkImage( + imageUrl: + "${AppConfig.assetsBaseURL}/${LevelUpConstants.starFileName}", + height: 24, + width: 24, ), ], ), ), ), + // Optional staging-only dropdown icon + SizedBox( + width: constraints.maxWidth >= 600 ? 120.0 : 65.0, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (Environment.isStagingEnvironment) + SizedBox( + width: 32.0, + height: 32.0, + child: Center( + child: IconButton( + icon: const Icon(Icons.arrow_drop_down), + style: IconButton.styleFrom( + padding: const EdgeInsets.all(4.0), + ), + onPressed: _toggleDetails, + constraints: const BoxConstraints(), + color: + Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + ), ], ), ), - ), - ), - ], + ); + }, + ), + ), + ), + ); + } +} + +class LevelUpPopup extends StatelessWidget { + const LevelUpPopup({ + super.key, + required this.widget, + }); + + final LevelUpBanner widget; + + @override + Widget build(BuildContext context) { + return FullWidthDialog( + maxWidth: 400, + maxHeight: 800, + dialogContent: Scaffold( + appBar: AppBar( + centerTitle: true, + title: kIsWeb + ? const Text( + "You have leveled up!", + style: TextStyle( + color: AppConfig.gold, + fontWeight: FontWeight.w600, + ), + ) + : null, + ), + body: LevelUpBarAnimation( + prevLevel: widget.prevLevel, + level: widget.level, ), ), ); @@ -312,12 +326,16 @@ class LevelUpBarAnimation extends StatefulWidget { class _LevelUpBarAnimationState extends State with SingleTickerProviderStateMixin { + late int _endGrammar; + late int _endVocab; late final AnimationController _controller; late final Animation _progressAnimation; late final Animation _vocabAnimation; late final Animation _grammarAnimation; late final Animation _skillsOpacity; late final Animation _shrinkMultiplier; + late final Uri? avatarUrl; + late final Future profile; late final ConfettiController _confettiController; @@ -327,10 +345,8 @@ class _LevelUpBarAnimationState extends State int displayedLevel = -1; bool _hasBlastedConfetti = false; - static const int _startGrammar = 23; - static const int _endGrammar = 78; - static const int _startVocab = 54; - static const int _endVocab = 64; + static const int _startGrammar = 0; + static const int _startVocab = 0; static const String language = "ES"; static const Duration _animationDuration = Duration(seconds: 6); @@ -344,6 +360,14 @@ class _LevelUpBarAnimationState extends State ConfettiController(duration: const Duration(seconds: 3)); _setConstructSummary(); + _setGrammarAndVocab(); + + final client = Matrix.of(context).client; + client.fetchOwnProfile().then((profile) { + setState(() { + avatarUrl = profile.avatarUrl; + }); + }); _controller = AnimationController( duration: _animationDuration, @@ -414,6 +438,20 @@ class _LevelUpBarAnimationState extends State } } + void _setGrammarAndVocab() { + _endGrammar = MatrixState.pangeaController.getAnalytics.constructListModel + .unlockedLemmas( + ConstructTypeEnum.morph, + ) + .length; + + _endVocab = MatrixState.pangeaController.getAnalytics.constructListModel + .unlockedLemmas( + ConstructTypeEnum.vocab, + ) + .length; + } + int _getSkillXP(LearningSkillsEnum skill) { return switch (skill) { LearningSkillsEnum.writing => @@ -451,20 +489,65 @@ class _LevelUpBarAnimationState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - // Avatar and language AnimatedBuilder( animation: _progressAnimation, builder: (_, __) => Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - ClipOval( - child: Image.asset( - './assets/favicon.png', - width: 200 * _shrinkMultiplier.value, - height: 200 * _shrinkMultiplier.value, - fit: BoxFit.cover, + avatarUrl == null + ? const CircularProgressIndicator() + : Padding( + padding: const EdgeInsets.all(24.0), + child: MxcImage( + uri: avatarUrl, + width: 150 * _shrinkMultiplier.value, + height: 150 * _shrinkMultiplier.value, + ), + ), + Text( + language, + style: TextStyle( + fontSize: 24 * _skillsOpacity.value, + color: AppConfig.goldLight, + fontWeight: FontWeight.bold, ), ), + ], + ), + ), + // Avatar and language + /*AnimatedBuilder( + animation: _progressAnimation, + builder: (_, __) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FutureBuilder( + future: client.fetchOwnProfile(), + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return const CircularProgressIndicator(); // Or a skeleton + } + if (snapshot.hasError) { + print("Profile fetch error: ${snapshot.error}"); + return const Text("Error loading profile"); + } + + if (!snapshot.hasData || snapshot.data == null) { + print("No profile data!"); + return const Text("No data"); + } + + + return ListTile( + leading: Avatar( + mxContent: snapshot.data?.avatarUrl, + size: 20, + ), + title: Text(profile?.displayName ?? client.userID!), + contentPadding: EdgeInsets.zero, + ); + }, + ), const SizedBox(width: 16), Text( language, @@ -476,7 +559,7 @@ class _LevelUpBarAnimationState extends State ), ], ), - ), + ),*/ // Progress bar + Level AnimatedBuilder( animation: _progressAnimation, @@ -558,7 +641,7 @@ class _LevelUpBarAnimationState extends State ), //const SizedBox(height: 16), Padding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.all(24.0), child: CachedNetworkImage( imageUrl: "${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}", From d49d08f67b4f4a95c91f59560f0ae57301a2aec2 Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Thu, 12 Jun 2025 12:37:39 -0400 Subject: [PATCH 07/32] Not functional, committing for code review --- lib/pages/chat/chat.dart | 27 +- .../settings_chat/settings_chat_view.dart | 4 +- .../level_up/level_up_banner.dart | 267 +++++++++++++++ .../level_up/level_up_manager.dart | 95 ++++++ .../level_up_popup.dart} | 308 +----------------- 5 files changed, 382 insertions(+), 319 deletions(-) create mode 100644 lib/pangea/analytics_misc/level_up/level_up_banner.dart create mode 100644 lib/pangea/analytics_misc/level_up/level_up_manager.dart rename lib/pangea/analytics_misc/{level_up.dart => level_up/level_up_popup.dart} (63%) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 7ee0881e0..d5d9f6c7f 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -4,22 +4,9 @@ import 'dart:async'; import 'dart:developer'; import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:collection/collection.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:matrix/matrix.dart'; -import 'package:scroll_to_index/scroll_to_index.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:universal_html/html.dart' as html; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; @@ -30,7 +17,7 @@ import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart'; -import 'package:fluffychat/pangea/analytics_misc/level_up.dart'; +import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_banner.dart'; import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart'; import 'package:fluffychat/pangea/chat/utils/unlocked_morphs_snackbar.dart'; import 'package:fluffychat/pangea/chat/widgets/event_too_large_dialog.dart'; @@ -66,6 +53,18 @@ import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart' import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/share_scaffold_dialog.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:matrix/matrix.dart'; +import 'package:scroll_to_index/scroll_to_index.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:universal_html/html.dart' as html; + import '../../utils/account_bundles.dart'; import '../../utils/localized_exception_extension.dart'; import 'send_file_dialog.dart'; diff --git a/lib/pages/settings_chat/settings_chat_view.dart b/lib/pages/settings_chat/settings_chat_view.dart index 74b163759..2773aaafb 100644 --- a/lib/pages/settings_chat/settings_chat_view.dart +++ b/lib/pages/settings_chat/settings_chat_view.dart @@ -8,7 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; //import for testing level up -import '../../pangea/analytics_misc/level_up.dart'; +import '../../pangea/analytics_misc/level_up/level_up_banner.dart'; import 'settings_chat.dart'; class SettingsChatView extends StatelessWidget { @@ -34,8 +34,8 @@ class SettingsChatView extends StatelessWidget { // Test button for leveling up onPressed: () { LevelUpUtil.showLevelUpDialog( - 5, 4, + 3, context, ); }, diff --git a/lib/pangea/analytics_misc/level_up/level_up_banner.dart b/lib/pangea/analytics_misc/level_up/level_up_banner.dart new file mode 100644 index 000000000..b7e41fbab --- /dev/null +++ b/lib/pangea/analytics_misc/level_up/level_up_banner.dart @@ -0,0 +1,267 @@ +import 'dart:async'; + +import 'package:audioplayers/audioplayers.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; +import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_manager.dart'; +import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_popup.dart'; +import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/pangea/common/utils/overlay.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; + +class LevelUpConstants { + static const String starFileName = "star.png"; + static const String dinoLevelUPFileName = "DinoBot-Congratulate.png"; +} + +class LevelUpUtil { + static Future showLevelUpDialog( + int level, + int prevLevel, + 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}", + ), + ) + .then( + (_) => Future.delayed( + const Duration(seconds: 2), + () => player.dispose(), + ), + ); + + OverlayUtil.showOverlay( + overlayKey: "level_up_notification", + context: context, + child: LevelUpBanner( + level: level, + prevLevel: prevLevel, + backButtonOverride: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + transformTargetId: '', + position: OverlayPositionEnum.top, + backDropToDismiss: false, + closePrevOverlay: false, + canPop: false, + ); + } +} + +class LevelUpBanner extends StatefulWidget { + final int level; + final int prevLevel; + final Widget? backButtonOverride; + + const LevelUpBanner({ + required this.level, + required this.prevLevel, + required this.backButtonOverride, + super.key, + }); + + @override + LevelUpBannerState createState() => LevelUpBannerState(); +} + +class LevelUpBannerState extends State + with TickerProviderStateMixin { + late AnimationController _slideController; + late Animation _slideAnimation; + + bool _showedDetails = false; + + @override + void initState() { + super.initState(); + + LevelUpManager().preloadAnalytics( + context, + widget.level, + widget.prevLevel, + ); + + LevelUpManager().shouldAutoPopup = true; + + LevelUpManager().printAnalytics(); + + _slideController = AnimationController( + vsync: this, + duration: FluffyThemes.animationDuration, + ); + + _slideAnimation = Tween( + begin: const Offset(0, -1), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _slideController, + curve: Curves.easeOut, + ), + ); + + _slideController.forward(); + + Future.delayed(const Duration(seconds: 10), () async { + if (mounted && !_showedDetails) {} + _close(); + }); + } + + Future _close() async { + await _slideController.reverse(); + MatrixState.pAnyState.closeOverlay("level_up_notification"); + } + + @override + void dispose() { + _slideController.dispose(); + super.dispose(); + } + + Future _toggleDetails() async { + await _close(); + LevelUpManager().markPopupSeen(); + _showedDetails = true; + + await showDialog( + context: context, + builder: (context) => LevelUpPopup(widget: widget), + ); + } + + @override + Widget build(BuildContext context) { + final isColumnMode = FluffyThemes.isColumnMode(context); + + final style = isColumnMode + ? Theme.of(context).textTheme.titleLarge?.copyWith( + color: AppConfig.gold, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ) + : Theme.of(context).textTheme.bodyLarge?.copyWith( + color: AppConfig.gold, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ); + + return SafeArea( + child: Material( + type: MaterialType.transparency, + child: SlideTransition( + position: _slideAnimation, + child: LayoutBuilder( + builder: (context, constraints) { + return GestureDetector( + onPanUpdate: (details) { + if (details.delta.dy < -10) _close(); + }, + onTap: _toggleDetails, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 16.0, + horizontal: 4.0, + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border( + bottom: BorderSide( + color: AppConfig.gold.withAlpha(200), + width: 2.0, + ), + ), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(AppConfig.borderRadius), + bottomRight: Radius.circular(AppConfig.borderRadius), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Spacer for symmetry + SizedBox( + width: constraints.maxWidth >= 600 ? 120.0 : 65.0, + ), + // Centered content + Expanded( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: isColumnMode ? 16.0 : 8.0, + ), + child: Wrap( + spacing: 16.0, + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + "Level up", + style: style, + overflow: TextOverflow.ellipsis, + ), + CachedNetworkImage( + imageUrl: + "${AppConfig.assetsBaseURL}/${LevelUpConstants.starFileName}", + height: 24, + width: 24, + ), + ], + ), + ), + ), + // Optional staging-only dropdown icon + SizedBox( + width: constraints.maxWidth >= 600 ? 120.0 : 65.0, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (Environment.isStagingEnvironment) + SizedBox( + width: 32.0, + height: 32.0, + child: Center( + child: IconButton( + icon: const Icon(Icons.arrow_drop_down), + style: IconButton.styleFrom( + padding: const EdgeInsets.all(4.0), + ), + onPressed: _toggleDetails, + constraints: const BoxConstraints(), + color: + Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/pangea/analytics_misc/level_up/level_up_manager.dart b/lib/pangea/analytics_misc/level_up/level_up_manager.dart new file mode 100644 index 000000000..a5c40f789 --- /dev/null +++ b/lib/pangea/analytics_misc/level_up/level_up_manager.dart @@ -0,0 +1,95 @@ +import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; +import 'package:fluffychat/pangea/constructs/construct_repo.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; + +class LevelUpManager { + LevelUpManager._internal(); + factory LevelUpManager() { + return _instance; + } + static final LevelUpManager _instance = LevelUpManager._internal(); + + int? prevLevel; + int? level; + + int? prevGrammar; + int? nextGrammar; + int? prevVocab; + int? nextVocab; + + ConstructSummary? constructSummary; + + bool hasSeenPopup = false; + bool shouldAutoPopup = false; + String? error; + + Future preloadAnalytics( + BuildContext context, + int level, + int prevLevel, + ) async { + this.level = level; + this.prevLevel = prevLevel; + + //grammar and vocab + nextGrammar = MatrixState.pangeaController.getAnalytics.constructListModel + .unlockedLemmas( + ConstructTypeEnum.morph, + ) + .length; + + nextVocab = MatrixState.pangeaController.getAnalytics.constructListModel + .unlockedLemmas( + ConstructTypeEnum.vocab, + ) + .length; + + //for now idk how to get these + prevGrammar = 0; + prevVocab = 0; + + //fetch construct summary + try { + constructSummary = await MatrixState.pangeaController.getAnalytics + .generateLevelUpAnalytics( + level, + prevLevel, + ); + } catch (e) { + error = e.toString(); + } + } + + void markPopupSeen() { + hasSeenPopup = true; + shouldAutoPopup = false; + } + + void printAnalytics() { + print('Level Up Analytics:'); + print('Current Level: $level'); + print('Previous Level: $prevLevel'); + print('Next Grammar: $nextGrammar'); + print('Next Vocab: $nextVocab'); + if (constructSummary != null) { + print('Construct Summary: ${constructSummary!.toJson()}'); + } else { + print('Construct Summary: Not available'); + } + } + + void reset() { + hasSeenPopup = false; + shouldAutoPopup = false; + prevLevel = null; + level = null; + prevGrammar = null; + nextGrammar = null; + prevVocab = null; + nextVocab = null; + constructSummary = null; + error = null; + // Reset any other state if necessary + } +} diff --git a/lib/pangea/analytics_misc/level_up.dart b/lib/pangea/analytics_misc/level_up/level_up_popup.dart similarity index 63% rename from lib/pangea/analytics_misc/level_up.dart rename to lib/pangea/analytics_misc/level_up/level_up_popup.dart index d4fc548b0..f582ba35a 100644 --- a/lib/pangea/analytics_misc/level_up.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_popup.dart @@ -1,18 +1,14 @@ import 'dart:async'; import 'dart:math'; -import 'package:audioplayers/audioplayers.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:confetti/confetti.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart'; +import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_banner.dart'; import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar.dart'; import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_details.dart'; -import 'package:fluffychat/pangea/common/config/environment.dart'; -import 'package:fluffychat/pangea/common/utils/overlay.dart'; import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart'; import 'package:fluffychat/pangea/constructs/construct_repo.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -23,257 +19,6 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:matrix/matrix_api_lite/generated/model.dart'; -class LevelUpConstants { - static const String starFileName = "star.png"; - static const String dinoLevelUPFileName = "DinoBot-Congratulate.png"; -} - -class LevelUpUtil { - static Future showLevelUpDialog( - int level, - int prevLevel, - 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}", - ), - ) - .then( - (_) => Future.delayed( - const Duration(seconds: 2), - () => player.dispose(), - ), - ); - - OverlayUtil.showOverlay( - overlayKey: "level_up_notification", - context: context, - child: LevelUpBanner( - level: level, - prevLevel: prevLevel, - backButtonOverride: IconButton( - icon: const Icon(Icons.close), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ), - transformTargetId: '', - position: OverlayPositionEnum.top, - backDropToDismiss: false, - closePrevOverlay: false, - canPop: false, - ); - } -} - -class LevelUpBanner extends StatefulWidget { - final int level; - final int prevLevel; - final Widget? backButtonOverride; - - const LevelUpBanner({ - required this.level, - required this.prevLevel, - required this.backButtonOverride, - super.key, - }); - - @override - LevelUpBannerState createState() => LevelUpBannerState(); -} - -class LevelUpBannerState extends State - with TickerProviderStateMixin { - late AnimationController _slideController; - late Animation _slideAnimation; - - late AnimationController _sizeController; - - final bool _showedDetails = false; - - @override - void initState() { - super.initState(); - - _slideController = AnimationController( - vsync: this, - duration: FluffyThemes.animationDuration, - ); - - _slideAnimation = Tween( - begin: const Offset(0, -1), - end: Offset.zero, - ).animate( - CurvedAnimation( - parent: _slideController, - curve: Curves.easeOut, - ), - ); - - _sizeController = AnimationController( - vsync: this, - duration: FluffyThemes.animationDuration, - ); - - _slideController.forward(); - - Future.delayed(const Duration(seconds: 10), () async { - if (mounted && !_showedDetails) _close(); - }); - } - - Future _close() async { - await _slideController.reverse(); - MatrixState.pAnyState.closeOverlay("level_up_notification"); - } - - @override - void dispose() { - _slideController.dispose(); - _sizeController.dispose(); - super.dispose(); - } - - Future _toggleDetails() async { - await _close(); - - //if (!mounted) return; - - await showDialog( - context: context, - builder: (context) => LevelUpPopup(widget: widget), - ); - } - - @override - Widget build(BuildContext context) { - final isColumnMode = FluffyThemes.isColumnMode(context); - - final style = isColumnMode - ? Theme.of(context).textTheme.titleLarge?.copyWith( - color: AppConfig.gold, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ) - : Theme.of(context).textTheme.bodyLarge?.copyWith( - color: AppConfig.gold, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ); - - return SafeArea( - child: Material( - type: MaterialType.transparency, - child: SlideTransition( - position: _slideAnimation, - child: LayoutBuilder( - builder: (context, constraints) { - return GestureDetector( - onPanUpdate: (details) { - if (details.delta.dy < -10) _close(); - }, - onTap: _toggleDetails, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 16.0, - horizontal: 4.0, - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - border: Border( - bottom: BorderSide( - color: AppConfig.gold.withAlpha(200), - width: 2.0, - ), - ), - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(AppConfig.borderRadius), - bottomRight: Radius.circular(AppConfig.borderRadius), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // Spacer for symmetry - SizedBox( - width: constraints.maxWidth >= 600 ? 120.0 : 65.0, - ), - // Centered content - Expanded( - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: isColumnMode ? 16.0 : 8.0, - ), - child: Wrap( - spacing: 16.0, - alignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text( - "Level up", - style: style, - overflow: TextOverflow.ellipsis, - ), - CachedNetworkImage( - imageUrl: - "${AppConfig.assetsBaseURL}/${LevelUpConstants.starFileName}", - height: 24, - width: 24, - ), - ], - ), - ), - ), - // Optional staging-only dropdown icon - SizedBox( - width: constraints.maxWidth >= 600 ? 120.0 : 65.0, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (Environment.isStagingEnvironment) - SizedBox( - width: 32.0, - height: 32.0, - child: Center( - child: IconButton( - icon: const Icon(Icons.arrow_drop_down), - style: IconButton.styleFrom( - padding: const EdgeInsets.all(4.0), - ), - onPressed: _toggleDetails, - constraints: const BoxConstraints(), - color: - Theme.of(context).colorScheme.onSurface, - ), - ), - ), - ], - ), - ), - ], - ), - ), - ); - }, - ), - ), - ), - ); - } -} - class LevelUpPopup extends StatelessWidget { const LevelUpPopup({ super.key, @@ -334,7 +79,7 @@ class _LevelUpBarAnimationState extends State late final Animation _grammarAnimation; late final Animation _skillsOpacity; late final Animation _shrinkMultiplier; - late final Uri? avatarUrl; + Uri? avatarUrl; late final Future profile; late final ConfettiController _confettiController; @@ -423,7 +168,9 @@ class _LevelUpBarAnimationState extends State ), ); - _controller.forward(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _controller.forward(); // or start your animation + }); } Future _setConstructSummary() async { @@ -515,51 +262,6 @@ class _LevelUpBarAnimationState extends State ], ), ), - // Avatar and language - /*AnimatedBuilder( - animation: _progressAnimation, - builder: (_, __) => Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FutureBuilder( - future: client.fetchOwnProfile(), - builder: (context, snapshot) { - if (snapshot.connectionState != ConnectionState.done) { - return const CircularProgressIndicator(); // Or a skeleton - } - if (snapshot.hasError) { - print("Profile fetch error: ${snapshot.error}"); - return const Text("Error loading profile"); - } - - if (!snapshot.hasData || snapshot.data == null) { - print("No profile data!"); - return const Text("No data"); - } - - - return ListTile( - leading: Avatar( - mxContent: snapshot.data?.avatarUrl, - size: 20, - ), - title: Text(profile?.displayName ?? client.userID!), - contentPadding: EdgeInsets.zero, - ); - }, - ), - const SizedBox(width: 16), - Text( - language, - style: TextStyle( - fontSize: 24 * _skillsOpacity.value, - color: AppConfig.goldLight, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ),*/ // Progress bar + Level AnimatedBuilder( animation: _progressAnimation, From fe22e2bcd283ead979d5a28f656c51a783a96d4d Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Fri, 13 Jun 2025 16:29:49 -0400 Subject: [PATCH 08/32] All real data, confetti tweaks, remove test button, timing and animation tweaks, added copy to arb file --- assets/l10n/intl_en.arb | 3 +- .../settings_chat/settings_chat_view.dart | 13 -- .../level_up/level_up_banner.dart | 58 ++--- .../level_up/level_up_manager.dart | 90 ++++--- .../level_up/level_up_popup.dart | 219 ++++++++---------- 5 files changed, 193 insertions(+), 190 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 899a6beb5..8eea060f9 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4976,5 +4976,6 @@ "canBeFoundViaCodeOrLink": "\u2022 code or link", "canBeFoundViaKnock": "\u2022 request to join and admin approval", "anyoneCanJoin": "Anyone can join! However, admin can kick and ban whoever misbehaves. Those who are banned may not return!", - "createYourSpace": "Create your space" + "createYourSpace": "Create your space", + "youHaveLeveledUp": "You have leveled up!" } diff --git a/lib/pages/settings_chat/settings_chat_view.dart b/lib/pages/settings_chat/settings_chat_view.dart index 2773aaafb..6d7013865 100644 --- a/lib/pages/settings_chat/settings_chat_view.dart +++ b/lib/pages/settings_chat/settings_chat_view.dart @@ -7,8 +7,6 @@ import 'package:fluffychat/widgets/settings_switch_list_tile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -//import for testing level up -import '../../pangea/analytics_misc/level_up/level_up_banner.dart'; import 'settings_chat.dart'; class SettingsChatView extends StatelessWidget { @@ -30,17 +28,6 @@ class SettingsChatView extends StatelessWidget { child: MaxWidthBody( child: Column( children: [ - ElevatedButton( - // Test button for leveling up - onPressed: () { - LevelUpUtil.showLevelUpDialog( - 4, - 3, - context, - ); - }, - child: const Text("Test Level Up Dialog"), - ), // #Pangea // SettingsSwitchListTile.adaptive( // title: L10n.of(context).formattedMessages, diff --git a/lib/pangea/analytics_misc/level_up/level_up_banner.dart b/lib/pangea/analytics_misc/level_up/level_up_banner.dart index b7e41fbab..186eafc77 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_banner.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_banner.dart @@ -23,29 +23,21 @@ class LevelUpUtil { int prevLevel, BuildContext context, ) async { + // Remove delay since GetAnalyticsController._onLevelUp is already async final player = AudioPlayer(); - final snackbarRegex = RegExp(r'_snackbar$'); + // Wait for any existing snackbars to dismiss + await _waitForSnackbars(context); - while (MatrixState.pAnyState.activeOverlays - .any((overlayId) => snackbarRegex.hasMatch(overlayId))) { - await Future.delayed(const Duration(milliseconds: 100)); - } + await player.play( + UrlSource( + "${AppConfig.assetsBaseURL}/${AnalyticsConstants.levelUpAudioFileName}", + ), + ); - player - .play( - UrlSource( - "${AppConfig.assetsBaseURL}/${AnalyticsConstants.levelUpAudioFileName}", - ), - ) - .then( - (_) => Future.delayed( - const Duration(seconds: 2), - () => player.dispose(), - ), - ); + if (!context.mounted) return; - OverlayUtil.showOverlay( + await OverlayUtil.showOverlay( overlayKey: "level_up_notification", context: context, child: LevelUpBanner( @@ -54,7 +46,7 @@ class LevelUpUtil { backButtonOverride: IconButton( icon: const Icon(Icons.close), onPressed: () { - Navigator.of(context).pop(); + MatrixState.pAnyState.closeOverlay("level_up_notification"); }, ), ), @@ -64,6 +56,17 @@ class LevelUpUtil { closePrevOverlay: false, canPop: false, ); + + await Future.delayed(const Duration(seconds: 2)); + player.dispose(); + } + + static Future _waitForSnackbars(BuildContext context) async { + final snackbarRegex = RegExp(r'_snackbar$'); + while (MatrixState.pAnyState.activeOverlays + .any((id) => snackbarRegex.hasMatch(id))) { + await Future.delayed(const Duration(milliseconds: 100)); + } } } @@ -94,15 +97,12 @@ class LevelUpBannerState extends State void initState() { super.initState(); - LevelUpManager().preloadAnalytics( + LevelUpManager.instance.preloadAnalytics( context, widget.level, widget.prevLevel, ); - - LevelUpManager().shouldAutoPopup = true; - - LevelUpManager().printAnalytics(); + LevelUpManager.instance.printAnalytics(); _slideController = AnimationController( vsync: this, @@ -122,8 +122,9 @@ class LevelUpBannerState extends State _slideController.forward(); Future.delayed(const Duration(seconds: 10), () async { - if (mounted && !_showedDetails) {} - _close(); + if (mounted && !_showedDetails) { + _close(); + } }); } @@ -140,12 +141,12 @@ class LevelUpBannerState extends State Future _toggleDetails() async { await _close(); - LevelUpManager().markPopupSeen(); + LevelUpManager.instance.markPopupSeen(); _showedDetails = true; await showDialog( context: context, - builder: (context) => LevelUpPopup(widget: widget), + builder: (context) => const LevelUpPopup(), ); } @@ -228,7 +229,6 @@ class LevelUpBannerState extends State ), ), ), - // Optional staging-only dropdown icon SizedBox( width: constraints.maxWidth >= 600 ? 120.0 : 65.0, child: Row( diff --git a/lib/pangea/analytics_misc/level_up/level_up_manager.dart b/lib/pangea/analytics_misc/level_up/level_up_manager.dart index a5c40f789..be8eb83cf 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_manager.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_manager.dart @@ -1,22 +1,25 @@ import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; +import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_banner.dart'; import 'package:fluffychat/pangea/constructs/construct_repo.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; class LevelUpManager { + // Singleton instance + static final LevelUpManager instance = LevelUpManager._internal(); + + // Private constructor LevelUpManager._internal(); - factory LevelUpManager() { - return _instance; - } - static final LevelUpManager _instance = LevelUpManager._internal(); - int? prevLevel; - int? level; + int prevLevel = 0; + int level = 0; - int? prevGrammar; - int? nextGrammar; - int? prevVocab; - int? nextVocab; + int prevGrammar = 0; + int nextGrammar = 0; + int prevVocab = 0; + int nextVocab = 0; + + String? userL2Code; ConstructSummary? constructSummary; @@ -24,6 +27,13 @@ class LevelUpManager { bool shouldAutoPopup = false; String? error; + bool _isShowingLevelUp = false; + + int get vocabCount => + MatrixState.pangeaController.getAnalytics.constructListModel + .unlockedLemmas(ConstructTypeEnum.vocab) + .length; + Future preloadAnalytics( BuildContext context, int level, @@ -32,22 +42,22 @@ class LevelUpManager { this.level = level; this.prevLevel = prevLevel; - //grammar and vocab - nextGrammar = MatrixState.pangeaController.getAnalytics.constructListModel - .unlockedLemmas( - ConstructTypeEnum.morph, - ) - .length; + shouldAutoPopup = true; - nextVocab = MatrixState.pangeaController.getAnalytics.constructListModel - .unlockedLemmas( - ConstructTypeEnum.vocab, - ) - .length; + //grammar and vocab + nextGrammar = MatrixState + .pangeaController.getAnalytics.constructListModel.grammarLemmas; + nextVocab = MatrixState + .pangeaController.getAnalytics.constructListModel.vocabLemmas; //for now idk how to get these - prevGrammar = 0; - prevVocab = 0; + prevGrammar = nextGrammar < 20 ? 0 : nextGrammar - 20; + + prevVocab = nextVocab < 20 ? 0 : nextVocab - 20; + + userL2Code = MatrixState.pangeaController.languageController + .activeL2Code() + ?.toUpperCase(); //fetch construct summary try { @@ -72,6 +82,8 @@ class LevelUpManager { print('Previous Level: $prevLevel'); print('Next Grammar: $nextGrammar'); print('Next Vocab: $nextVocab'); + print("should show popup: $shouldAutoPopup"); + print("has seen popup: $hasSeenPopup"); if (constructSummary != null) { print('Construct Summary: ${constructSummary!.toJson()}'); } else { @@ -79,17 +91,37 @@ class LevelUpManager { } } + Future handleLevelUp( + BuildContext context, + int level, + int prevLevel, + ) async { + if (_isShowingLevelUp) return; + _isShowingLevelUp = true; + + await preloadAnalytics(context, level, prevLevel); + + if (!context.mounted) { + _isShowingLevelUp = false; + return; + } + + await LevelUpUtil.showLevelUpDialog(level, prevLevel, context); + _isShowingLevelUp = false; + } + void reset() { hasSeenPopup = false; shouldAutoPopup = false; - prevLevel = null; - level = null; - prevGrammar = null; - nextGrammar = null; - prevVocab = null; - nextVocab = null; + prevLevel = 0; + level = 0; + prevGrammar = 0; + nextGrammar = 0; + prevVocab = 0; + nextVocab = 0; constructSummary = null; error = null; + _isShowingLevelUp = false; // Reset any other state if necessary } } diff --git a/lib/pangea/analytics_misc/level_up/level_up_popup.dart b/lib/pangea/analytics_misc/level_up/level_up_popup.dart index f582ba35a..977183b62 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_popup.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_popup.dart @@ -4,10 +4,10 @@ import 'dart:math'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:confetti/confetti.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_banner.dart'; -import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar.dart'; +import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_manager.dart'; +import 'package:fluffychat/pangea/analytics_summary/progress_bar/level_bar.dart'; import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_details.dart'; import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart'; import 'package:fluffychat/pangea/constructs/construct_repo.dart'; @@ -22,11 +22,8 @@ import 'package:matrix/matrix_api_lite/generated/model.dart'; class LevelUpPopup extends StatelessWidget { const LevelUpPopup({ super.key, - required this.widget, }); - final LevelUpBanner widget; - @override Widget build(BuildContext context) { return FullWidthDialog( @@ -36,18 +33,18 @@ class LevelUpPopup extends StatelessWidget { appBar: AppBar( centerTitle: true, title: kIsWeb - ? const Text( - "You have leveled up!", - style: TextStyle( + ? Text( + L10n.of(context).youHaveLeveledUp, + style: const TextStyle( color: AppConfig.gold, fontWeight: FontWeight.w600, ), ) : null, ), - body: LevelUpBarAnimation( - prevLevel: widget.prevLevel, - level: widget.level, + body: LevelUpPopupContent( + prevLevel: LevelUpManager.instance.prevLevel ?? 0, + level: LevelUpManager.instance.level ?? 0, ), ), ); @@ -55,57 +52,55 @@ class LevelUpPopup extends StatelessWidget { } //animated progress bar -- move to own file later -class LevelUpBarAnimation extends StatefulWidget { +class LevelUpPopupContent extends StatefulWidget { final int prevLevel; final int level; - const LevelUpBarAnimation({ + const LevelUpPopupContent({ super.key, required this.prevLevel, required this.level, }); @override - State createState() => _LevelUpBarAnimationState(); + State createState() => _LevelUpPopupContentState(); } -class _LevelUpBarAnimationState extends State +class _LevelUpPopupContentState extends State with SingleTickerProviderStateMixin { late int _endGrammar; late int _endVocab; late final AnimationController _controller; - late final Animation _progressAnimation; - late final Animation _vocabAnimation; - late final Animation _grammarAnimation; - late final Animation _skillsOpacity; - late final Animation _shrinkMultiplier; + Uri? avatarUrl; late final Future profile; late final ConfettiController _confettiController; - ConstructSummary? _constructSummary; - String? _error; - int displayedLevel = -1; bool _hasBlastedConfetti = false; - static const int _startGrammar = 0; - static const int _startVocab = 0; - static const String language = "ES"; + static final int _startGrammar = LevelUpManager.instance.prevGrammar ?? 0; + static final int _startVocab = LevelUpManager.instance.prevVocab ?? 0; + static final ConstructSummary? _constructSummary = + LevelUpManager.instance.constructSummary; + static final String? _error = LevelUpManager.instance.error; + static final String language = LevelUpManager.instance.userL2Code ?? "N/A"; - static const Duration _animationDuration = Duration(seconds: 6); + static const Duration _animationDuration = Duration(seconds: 5); @override void initState() { super.initState(); + LevelUpManager.instance.markPopupSeen(); displayedLevel = widget.prevLevel; _confettiController = ConfettiController(duration: const Duration(seconds: 3)); - _setConstructSummary(); - _setGrammarAndVocab(); + // Use LevelUpManager stats instead of fetching separately + _endGrammar = LevelUpManager.instance.nextGrammar ?? 0; + _endVocab = LevelUpManager.instance.nextVocab ?? 0; final client = Matrix.of(context).client; client.fetchOwnProfile().then((profile) { @@ -135,70 +130,10 @@ class _LevelUpBarAnimationState extends State } }); - _progressAnimation = Tween(begin: 0, end: 1).animate( - CurvedAnimation(parent: _controller, curve: Curves.easeOutBack), - ); - - _vocabAnimation = IntTween(begin: _startVocab, end: _endVocab).animate( - CurvedAnimation( - parent: _controller, - curve: const Interval(0.0, 0.5, curve: Curves.easeInOutQuad), - ), - ); - - _grammarAnimation = - IntTween(begin: _startGrammar, end: _endGrammar).animate( - CurvedAnimation( - parent: _controller, - curve: const Interval(0.0, 0.5, curve: Curves.easeInOutQuad), - ), - ); - - _skillsOpacity = Tween(begin: 0.0, end: 1.0).animate( - CurvedAnimation( - parent: _controller, - curve: const Interval(0.7, 1.0, curve: Curves.easeIn), - ), - ); - - _shrinkMultiplier = Tween(begin: 1.0, end: 0.3).animate( - CurvedAnimation( - parent: _controller, - curve: const Interval(0.7, 1.0, curve: Curves.easeInOut), - ), - ); - - WidgetsBinding.instance.addPostFrameCallback((_) { - _controller.forward(); // or start your animation - }); - } - - Future _setConstructSummary() async { - try { - _constructSummary = await MatrixState.pangeaController.getAnalytics - .generateLevelUpAnalytics( - widget.level, - widget.prevLevel, - ); - } catch (e) { - _error = e.toString(); - } - } - - void _setGrammarAndVocab() { - _endGrammar = MatrixState.pangeaController.getAnalytics.constructListModel - .unlockedLemmas( - ConstructTypeEnum.morph, - ) - .length; - - _endVocab = MatrixState.pangeaController.getAnalytics.constructListModel - .unlockedLemmas( - ConstructTypeEnum.vocab, - ) - .length; + _controller.forward(); } + // Use LevelUpManager's constructSummary instead of local _constructSummary int _getSkillXP(LearningSkillsEnum skill) { return switch (skill) { LearningSkillsEnum.writing => @@ -217,12 +152,50 @@ class _LevelUpBarAnimationState extends State void dispose() { _controller.dispose(); _confettiController.dispose(); + LevelUpManager.instance.reset(); super.dispose(); } @override @override Widget build(BuildContext context) { + final Animation progressAnimation = + Tween(begin: 0, end: 1).animate( + CurvedAnimation(parent: _controller, curve: const Interval(0.0, 0.5)), + ); + + final Animation vocabAnimation = + IntTween(begin: _startVocab, end: _endVocab).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.5, curve: Curves.easeInOutQuad), + ), + ); + + final Animation grammarAnimation = + IntTween(begin: _startGrammar, end: _endGrammar).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.5, curve: Curves.easeInOutQuad), + ), + ); + + final Animation skillsOpacity = + Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.7, 1.0, curve: Curves.easeIn), + ), + ); + + final Animation shrinkMultiplier = + Tween(begin: 1.0, end: 0.3).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.7, 1.0, curve: Curves.easeInOut), + ), + ); + final colorScheme = Theme.of(context).colorScheme; final grammarVocabStyle = Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, @@ -237,24 +210,26 @@ class _LevelUpBarAnimationState extends State crossAxisAlignment: CrossAxisAlignment.center, children: [ AnimatedBuilder( - animation: _progressAnimation, + animation: _controller, builder: (_, __) => Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - avatarUrl == null - ? const CircularProgressIndicator() - : Padding( - padding: const EdgeInsets.all(24.0), - child: MxcImage( - uri: avatarUrl, - width: 150 * _shrinkMultiplier.value, - height: 150 * _shrinkMultiplier.value, + Padding( + padding: const EdgeInsets.all(24.0), + child: avatarUrl == null + ? const CircularProgressIndicator() + : ClipOval( + child: MxcImage( + uri: avatarUrl, + width: 150 * shrinkMultiplier.value, + height: 150 * shrinkMultiplier.value, + ), ), - ), + ), Text( language, style: TextStyle( - fontSize: 24 * _skillsOpacity.value, + fontSize: 24 * skillsOpacity.value, color: AppConfig.goldLight, fontWeight: FontWeight.bold, ), @@ -264,19 +239,26 @@ class _LevelUpBarAnimationState extends State ), // Progress bar + Level AnimatedBuilder( - animation: _progressAnimation, + animation: _controller, builder: (_, __) => Row( children: [ Expanded( - child: ProgressBar( - levelBars: [ - LevelBarDetails( - widthMultiplier: _progressAnimation.value, - currentPoints: 0, - fillColor: AppConfig.goldLight, - ), - ], - height: 20, + child: LayoutBuilder( + builder: (context, constraints) { + return LevelBar( + details: const LevelBarDetails( + fillColor: Colors.green, + currentPoints: 0, + widthMultiplier: 1, + ), + progressBarDetails: ProgressBarDetails( + totalWidth: constraints.maxWidth * + progressAnimation.value, + height: 20, + borderColor: colorScheme.surface, + ), + ); + }, ), ), const SizedBox(width: 8), @@ -304,7 +286,7 @@ class _LevelUpBarAnimationState extends State size: 35, ), const SizedBox(width: 8), - Text('${_vocabAnimation.value}', style: grammarVocabStyle), + Text('${vocabAnimation.value}', style: grammarVocabStyle), const SizedBox(width: 40), Icon( Symbols.toys_and_games, @@ -313,7 +295,7 @@ class _LevelUpBarAnimationState extends State ), const SizedBox(width: 8), Text( - '${_grammarAnimation.value}', + '${grammarAnimation.value}', style: grammarVocabStyle, ), ], @@ -323,9 +305,9 @@ class _LevelUpBarAnimationState extends State // Skills section AnimatedBuilder( - animation: _skillsOpacity, + animation: skillsOpacity, builder: (_, __) => Opacity( - opacity: _skillsOpacity.value, + opacity: skillsOpacity.value, child: _error == null ? Column( crossAxisAlignment: CrossAxisAlignment.center, @@ -415,8 +397,9 @@ class _LevelUpBarAnimationState extends State .explosive, // don't specify a direction, blast randomly shouldLoop: true, // start again as soon as the animation is finished - emissionFrequency: 0.1, - numberOfParticles: 7, + emissionFrequency: 0.2, + numberOfParticles: 15, + gravity: 0.1, colors: const [ AppConfig.goldLight, AppConfig.gold, From 64aba1d6e4b5999c6db33ded9e6d009ab138fe1c Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Wed, 18 Jun 2025 14:13:53 -0400 Subject: [PATCH 09/32] Add rain confetti and animated flip counter Change continuous blast of confetti to one blast with rain and a new animation type, and changed skills names for cleaner skills table look --- lib/l10n/intl_en.arb | 8 +- .../level_up/level_up_popup.dart | 86 +++++++------ .../level_up/rain_confetti.dart | 120 ++++++++++++++++++ 3 files changed, 168 insertions(+), 46 deletions(-) create mode 100644 lib/pangea/analytics_misc/level_up/rain_confetti.dart diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 6f9712e16..827817645 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -4630,9 +4630,9 @@ "meaningSectionHeader": "Meaning:", "formSectionHeader": "Forms used in chats:", "noEmojiSelectedTooltip": "No emoji selected", - "writingExercisesTooltip": "Writing practice", - "listeningExercisesTooltip": "Listening practice", - "readingExercisesTooltip": "Reading practice", + "writingExercisesTooltip": "Writing", + "listeningExercisesTooltip": "Listening", + "readingExercisesTooltip": "Reading", "meaningNotFound": "Meaning could not be found.", "formsNotFound": "Forms could not be found.", "chooseBaseForm": "Choose the base form", @@ -5016,6 +5016,6 @@ "groupChat": "Group Chat", "directMessage": "Direct Message", "newDirectMessage": "New direct message", - "speakingExercisesTooltip": "Speaking practice", + "speakingExercisesTooltip": "Speaking", "noChatsFoundHereYet": "No chats found here yet" } diff --git a/lib/pangea/analytics_misc/level_up/level_up_popup.dart b/lib/pangea/analytics_misc/level_up/level_up_popup.dart index 977183b62..4ef50782e 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_popup.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_popup.dart @@ -1,12 +1,15 @@ import 'dart:async'; import 'dart:math'; +import 'package:animated_flip_counter/animated_flip_counter.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:confetti/confetti.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_banner.dart'; import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_manager.dart'; +import 'package:fluffychat/pangea/analytics_misc/level_up/rain_confetti.dart'; import 'package:fluffychat/pangea/analytics_summary/progress_bar/level_bar.dart'; import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_details.dart'; import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart'; @@ -15,7 +18,6 @@ import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:matrix/matrix_api_lite/generated/model.dart'; @@ -43,8 +45,8 @@ class LevelUpPopup extends StatelessWidget { : null, ), body: LevelUpPopupContent( - prevLevel: LevelUpManager.instance.prevLevel ?? 0, - level: LevelUpManager.instance.level ?? 0, + prevLevel: LevelUpManager.instance.prevLevel, + level: LevelUpManager.instance.level, ), ), ); @@ -80,12 +82,11 @@ class _LevelUpPopupContentState extends State int displayedLevel = -1; bool _hasBlastedConfetti = false; - static final int _startGrammar = LevelUpManager.instance.prevGrammar ?? 0; - static final int _startVocab = LevelUpManager.instance.prevVocab ?? 0; - static final ConstructSummary? _constructSummary = - LevelUpManager.instance.constructSummary; - static final String? _error = LevelUpManager.instance.error; - static final String language = LevelUpManager.instance.userL2Code ?? "N/A"; + final int _startGrammar = LevelUpManager.instance.prevGrammar; + final int _startVocab = LevelUpManager.instance.prevVocab; + late ConstructSummary? _constructSummary; + final String? _error = LevelUpManager.instance.error; + String language = LevelUpManager.instance.userL2Code ?? "N/A"; static const Duration _animationDuration = Duration(seconds: 5); @@ -96,11 +97,12 @@ class _LevelUpPopupContentState extends State displayedLevel = widget.prevLevel; _confettiController = - ConfettiController(duration: const Duration(seconds: 3)); + ConfettiController(duration: const Duration(seconds: 1)); // Use LevelUpManager stats instead of fetching separately - _endGrammar = LevelUpManager.instance.nextGrammar ?? 0; - _endVocab = LevelUpManager.instance.nextVocab ?? 0; + _endGrammar = LevelUpManager.instance.nextGrammar; + _endVocab = LevelUpManager.instance.nextVocab; + _constructSummary = LevelUpManager.instance.constructSummary; final client = Matrix.of(context).client; client.fetchOwnProfile().then((profile) { @@ -124,9 +126,10 @@ class _LevelUpPopupContentState extends State }); _controller.addListener(() { - if (_controller.value >= 0.4 && !_hasBlastedConfetti) { - _confettiController.play(); + if (_controller.value >= 0.5 && !_hasBlastedConfetti) { + //_confettiController.play(); _hasBlastedConfetti = true; + rainConfetti(context); } }); @@ -153,6 +156,7 @@ class _LevelUpPopupContentState extends State _controller.dispose(); _confettiController.dispose(); LevelUpManager.instance.reset(); + stopConfetti(); super.dispose(); } @@ -217,7 +221,12 @@ class _LevelUpPopupContentState extends State Padding( padding: const EdgeInsets.all(24.0), child: avatarUrl == null - ? const CircularProgressIndicator() + ? MxcImage( + client: Matrix.of(context).client, + fit: BoxFit.cover, + width: 150 * shrinkMultiplier.value, + height: 150 * shrinkMultiplier.value, + ) : ClipOval( child: MxcImage( uri: avatarUrl, @@ -255,7 +264,7 @@ class _LevelUpPopupContentState extends State totalWidth: constraints.maxWidth * progressAnimation.value, height: 20, - borderColor: colorScheme.surface, + borderColor: colorScheme.primary, ), ); }, @@ -263,11 +272,23 @@ class _LevelUpPopupContentState extends State ), const SizedBox(width: 8), Text( - "⭐ $displayedLevel", - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - color: AppConfig.goldLight, - ), + "⭐", + style: Theme.of(context).textTheme.titleLarge, + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: AnimatedFlipCounter( + value: displayedLevel, + textStyle: Theme.of(context) + .textTheme + .headlineMedium + ?.copyWith( + fontWeight: FontWeight.bold, + color: AppConfig.goldLight, + ), + duration: const Duration(milliseconds: 1000), + curve: Curves.easeInOut, + ), ), ], ), @@ -388,35 +409,16 @@ class _LevelUpPopupContentState extends State ], ), ), - // Confetti overlay - Align( - alignment: Alignment.topCenter, - child: ConfettiWidget( - confettiController: _confettiController, - blastDirectionality: BlastDirectionality - .explosive, // don't specify a direction, blast randomly - shouldLoop: - true, // start again as soon as the animation is finished - emissionFrequency: 0.2, - numberOfParticles: 15, - gravity: 0.1, - colors: const [ - AppConfig.goldLight, - AppConfig.gold, - ], // manually specify the colors to be used - createParticlePath: drawStar, // define a custom shape/path. - ), - ), ], ); } Widget _buildSkillsTable(BuildContext context) { final visibleSkills = LearningSkillsEnum.values - .where((skill) => _getSkillXP(skill) > -1) + .where((skill) => (_getSkillXP(skill) > -1) && skill.isVisible) .toList(); - const itemsPerRow = 3; + const itemsPerRow = 4; // chunk into rows of up to 3 final rows = >[ for (var i = 0; i < visibleSkills.length; i += itemsPerRow) diff --git a/lib/pangea/analytics_misc/level_up/rain_confetti.dart b/lib/pangea/analytics_misc/level_up/rain_confetti.dart new file mode 100644 index 000000000..f4d3d7df4 --- /dev/null +++ b/lib/pangea/analytics_misc/level_up/rain_confetti.dart @@ -0,0 +1,120 @@ +import 'dart:math'; + +import 'package:confetti/confetti.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:flutter/material.dart'; + +OverlayEntry? _confettiEntry; +ConfettiController? _blastController; +ConfettiController? _rainController; + +void rainConfetti(BuildContext context) { + if (_confettiEntry != null) return; // Prevent duplicates + + _blastController = ConfettiController(duration: const Duration(seconds: 1)); + _rainController = ConfettiController(duration: const Duration(seconds: 3)); + + _blastController!.play(); + _rainController!.play(); + + final screenWidth = MediaQuery.of(context).size.width; + final screenHeight = MediaQuery.of(context).size.height; + final isSmallScreen = screenWidth < 600; + final count = isSmallScreen ? 2 : 5; + final spacing = screenWidth / (count + 1); + + _confettiEntry = OverlayEntry( + builder: (context) => Stack( + children: [ + // Initial center blast + Positioned( + top: 0, + left: screenWidth / 2, + child: IgnorePointer( + child: ConfettiWidget( + confettiController: _blastController!, + blastDirectionality: BlastDirectionality.explosive, + shouldLoop: false, + emissionFrequency: .02, + numberOfParticles: 40, + minimumSize: const Size(20, 20), + maximumSize: const Size(25, 25), + minBlastForce: 10, + maxBlastForce: 40, + gravity: 0.07, + colors: const [AppConfig.goldLight, AppConfig.gold], + createParticlePath: drawStar, + ), + ), + ), + + // Rain confetti from the top + ...List.generate(count, (index) { + final left = spacing * (index + 1) - 10; + + return Positioned( + top: -30, // Small buffer above top edge + left: left, + child: IgnorePointer( + child: ConfettiWidget( + confettiController: _rainController!, + blastDirectionality: BlastDirectionality.directional, + blastDirection: 3 * pi / 2, + shouldLoop: true, + maxBlastForce: 5, + minBlastForce: 2, + minimumSize: const Size(20, 20), + maximumSize: const Size(25, 25), + gravity: 0.07, + emissionFrequency: 0.1, + numberOfParticles: 2, + colors: const [AppConfig.goldLight, AppConfig.gold], + createParticlePath: drawStar, + ), + ), + ); + }), + ], + ), + ); + + Overlay.of(context, rootOverlay: true).insert(_confettiEntry!); +} + +void stopConfetti() { + _confettiEntry?.remove(); + _confettiEntry = null; + + _blastController?.dispose(); + _blastController = null; + + _rainController?.dispose(); + _rainController = null; +} + +Path drawStar(Size size) { + double degToRad(double deg) => deg * (pi / 180.0); + + const numberOfPoints = 5; + final halfWidth = size.width / 2; + final externalRadius = halfWidth; + final internalRadius = halfWidth / 2.5; + final degreesPerStep = degToRad(360 / numberOfPoints); + final halfDegreesPerStep = degreesPerStep / 2; + final path = Path(); + final fullAngle = degToRad(360); + path.moveTo(size.width, halfWidth); + + for (double step = 0; step < fullAngle; step += degreesPerStep) { + path.lineTo( + halfWidth + externalRadius * cos(step), + halfWidth + externalRadius * sin(step), + ); + path.lineTo( + halfWidth + internalRadius * cos(step + halfDegreesPerStep), + halfWidth + internalRadius * sin(step + halfDegreesPerStep), + ); + } + path.close(); + return path; +} From 61c60f5ff626139e18afd34f585d8ba0552097ab Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Wed, 18 Jun 2025 16:49:02 -0400 Subject: [PATCH 10/32] Fix skill XP from constructSummary not being generated bug Also added commented out code to fetch last construct summary instead of generate a new one for testing --- .../get_analytics_controller.dart | 22 +++-- .../level_up/level_up_banner.dart | 1 - .../level_up/level_up_manager.dart | 93 +++++++++++-------- lib/utils/client_manager.dart | 11 +-- pubspec.yaml | 1 + 5 files changed, 66 insertions(+), 62 deletions(-) diff --git a/lib/pangea/analytics_misc/get_analytics_controller.dart b/lib/pangea/analytics_misc/get_analytics_controller.dart index dcdcafd65..3413cc9f5 100644 --- a/lib/pangea/analytics_misc/get_analytics_controller.dart +++ b/lib/pangea/analytics_misc/get_analytics_controller.dart @@ -1,11 +1,5 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - -import 'package:get_storage/get_storage.dart'; -import 'package:matrix/matrix.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; - import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_list_model.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; @@ -22,6 +16,10 @@ 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'; +import 'package:flutter/material.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:matrix/matrix.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; /// A minimized version of AnalyticsController that get the logged in user's analytics class GetAnalyticsController extends BaseController { @@ -455,10 +453,13 @@ class GetAnalyticsController extends BaseController { // int diffXP = maxXP - minXP; // if (diffXP < 0) diffXP = 0; - Future getConstructSummaryFromStateEvent() async { + ConstructSummary? getConstructSummaryFromStateEvent() { try { final Room? analyticsRoom = _client.analyticsRoomLocal(_l2!); - if (analyticsRoom == null) return null; + if (analyticsRoom == null) { + debugPrint("Analytics room is null"); + return null; + } final state = analyticsRoom.getState(PangeaEventTypes.constructSummary, ''); if (state == null) return null; @@ -477,9 +478,10 @@ class GetAnalyticsController extends BaseController { // generate level up analytics as a construct summary ConstructSummary summary; try { - final int maxXP = constructListModel.calculateXpWithLevel(upperLevel); - final int minXP = constructListModel.calculateXpWithLevel(lowerLevel); + final int maxXP = constructListModel.calculateXpWithLevel(lowerLevel); + final int minXP = constructListModel.calculateXpWithLevel(upperLevel); int diffXP = maxXP - minXP; + debugPrint("minXP: $minXP, maxXP: $maxXP, diffXP: $diffXP"); if (diffXP < 0) diffXP = 0; // compute construct use of current level diff --git a/lib/pangea/analytics_misc/level_up/level_up_banner.dart b/lib/pangea/analytics_misc/level_up/level_up_banner.dart index 186eafc77..ad0b840e4 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_banner.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_banner.dart @@ -102,7 +102,6 @@ class LevelUpBannerState extends State widget.level, widget.prevLevel, ); - LevelUpManager.instance.printAnalytics(); _slideController = AnimationController( vsync: this, diff --git a/lib/pangea/analytics_misc/level_up/level_up_manager.dart b/lib/pangea/analytics_misc/level_up/level_up_manager.dart index be8eb83cf..806ad7477 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_manager.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_manager.dart @@ -1,5 +1,4 @@ import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; -import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_banner.dart'; import 'package:fluffychat/pangea/constructs/construct_repo.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -27,8 +26,6 @@ class LevelUpManager { bool shouldAutoPopup = false; String? error; - bool _isShowingLevelUp = false; - int get vocabCount => MatrixState.pangeaController.getAnalytics.constructListModel .unlockedLemmas(ConstructTypeEnum.vocab) @@ -51,15 +48,46 @@ class LevelUpManager { .pangeaController.getAnalytics.constructListModel.vocabLemmas; //for now idk how to get these - prevGrammar = nextGrammar < 20 ? 0 : nextGrammar - 20; + prevGrammar = nextGrammar < 30 ? 0 : nextGrammar - 30; - prevVocab = nextVocab < 20 ? 0 : nextVocab - 20; + prevVocab = nextVocab < 30 ? 0 : nextVocab - 30; userL2Code = MatrixState.pangeaController.languageController .activeL2Code() ?.toUpperCase(); - //fetch construct summary + /*for testing, just fetch last level up + constructSummary = MatrixState.pangeaController.getAnalytics + .getConstructSummaryFromStateEvent(); + debugPrint( + "Last saved construct summary: ${constructSummary?.toJson()}", + ); + + final client = MatrixState.pangeaController.matrixState.client; + + final Room? analyticsRoom = client.analyticsRoomLocal( + MatrixState.pangeaController.languageController.userL2!, + ); + + // Get all summary events in the timeline + final timeline = await analyticsRoom!.getTimeline(); + final summaryEvents = timeline.events + .where( + (e) => e.type == PangeaEventTypes.constructSummary, + ) + .map( + (e) => ConstructSummary.fromJson(e.content), + ) + .toList(); + debugPrint("List of previous summaries from timeline: $summaryEvents"); + + for (final summary in summaryEvents) { + debugPrint("Individual summaries from timeline: ${summary.toJson()}"); + } + + */ + + // fetch construct summary for actual app, not while testing since level up isn't true try { constructSummary = await MatrixState.pangeaController.getAnalytics .generateLevelUpAnalytics( @@ -69,6 +97,24 @@ class LevelUpManager { } catch (e) { error = e.toString(); } + // end of that block + await Future.delayed( + const Duration(seconds: 1), + () => LevelUpManager.instance.printAnalytics(), + ); + } + + void printAnalytics() { + debugPrint('Level Up Analytics:'); + debugPrint('Current Level: $level'); + debugPrint('Previous Level: $prevLevel'); + debugPrint('Next Grammar: $nextGrammar'); + debugPrint('Next Vocab: $nextVocab'); + if (constructSummary != null) { + debugPrint('Construct Summary: ${constructSummary!.toJson()}'); + } else { + debugPrint('Construct Summary: Not available'); + } } void markPopupSeen() { @@ -76,40 +122,6 @@ class LevelUpManager { shouldAutoPopup = false; } - void printAnalytics() { - print('Level Up Analytics:'); - print('Current Level: $level'); - print('Previous Level: $prevLevel'); - print('Next Grammar: $nextGrammar'); - print('Next Vocab: $nextVocab'); - print("should show popup: $shouldAutoPopup"); - print("has seen popup: $hasSeenPopup"); - if (constructSummary != null) { - print('Construct Summary: ${constructSummary!.toJson()}'); - } else { - print('Construct Summary: Not available'); - } - } - - Future handleLevelUp( - BuildContext context, - int level, - int prevLevel, - ) async { - if (_isShowingLevelUp) return; - _isShowingLevelUp = true; - - await preloadAnalytics(context, level, prevLevel); - - if (!context.mounted) { - _isShowingLevelUp = false; - return; - } - - await LevelUpUtil.showLevelUpDialog(level, prevLevel, context); - _isShowingLevelUp = false; - } - void reset() { hasSeenPopup = false; shouldAutoPopup = false; @@ -121,7 +133,6 @@ class LevelUpManager { nextVocab = 0; constructSummary = null; error = null; - _isShowingLevelUp = false; // Reset any other state if necessary } } diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index bde4dd1c5..01bcb49c4 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -1,17 +1,7 @@ import 'dart:io'; -import 'package:flutter/foundation.dart'; - import 'package:collection/collection.dart'; import 'package:desktop_notifications/desktop_notifications.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:matrix/encryption/utils/key_verification.dart'; -import 'package:matrix/matrix.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:universal_html/html.dart' as html; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/pangea/common/constants/model_keys.dart'; @@ -129,6 +119,7 @@ abstract class ClientManager { PangeaEventTypes.userSetLemmaInfo, EventTypes.RoomJoinRules, PangeaEventTypes.activityPlan, + PangeaEventTypes.constructSummary, // Pangea# }, logLevel: kReleaseMode ? Level.warning : Level.verbose, diff --git a/pubspec.yaml b/pubspec.yaml index 9802e4819..347c208e6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -136,6 +136,7 @@ dependencies: text_to_speech: git: https://github.com/pangeachat/text_to_speech.git flutter_tts: ^4.2.0 + animated_flip_counter: ^0.3.4 # Pangea# dev_dependencies: From 21e3d17c5da205252674b38f64eeeaf60bdbf328 Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Fri, 20 Jun 2025 11:46:55 -0400 Subject: [PATCH 11/32] Add polling for construct summary to display when generated and slight refactoring of level up popup --- lib/pages/chat/chat.dart | 1 - .../settings_chat/settings_chat_view.dart | 1 - .../level_up/level_up_popup.dart | 57 +++++++++++-------- 3 files changed, 33 insertions(+), 26 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 9339c8537..36f547729 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -60,7 +60,6 @@ import 'package:fluffychat/widgets/share_scaffold_dialog.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:matrix/matrix.dart'; diff --git a/lib/pages/settings_chat/settings_chat_view.dart b/lib/pages/settings_chat/settings_chat_view.dart index 9942efa8a..58eec8d46 100644 --- a/lib/pages/settings_chat/settings_chat_view.dart +++ b/lib/pages/settings_chat/settings_chat_view.dart @@ -6,7 +6,6 @@ import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/settings_switch_list_tile.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'settings_chat.dart'; diff --git a/lib/pangea/analytics_misc/level_up/level_up_popup.dart b/lib/pangea/analytics_misc/level_up/level_up_popup.dart index 4ef50782e..4055cbe85 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_popup.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_popup.dart @@ -53,7 +53,6 @@ class LevelUpPopup extends StatelessWidget { } } -//animated progress bar -- move to own file later class LevelUpPopupContent extends StatefulWidget { final int prevLevel; final int level; @@ -85,6 +84,7 @@ class _LevelUpPopupContentState extends State final int _startGrammar = LevelUpManager.instance.prevGrammar; final int _startVocab = LevelUpManager.instance.prevVocab; late ConstructSummary? _constructSummary; + Timer? _summaryPollTimer; final String? _error = LevelUpManager.instance.error; String language = LevelUpManager.instance.userL2Code ?? "N/A"; @@ -94,23 +94,31 @@ class _LevelUpPopupContentState extends State void initState() { super.initState(); LevelUpManager.instance.markPopupSeen(); - displayedLevel = widget.prevLevel; _confettiController = ConfettiController(duration: const Duration(seconds: 1)); - - // Use LevelUpManager stats instead of fetching separately _endGrammar = LevelUpManager.instance.nextGrammar; _endVocab = LevelUpManager.instance.nextVocab; _constructSummary = LevelUpManager.instance.constructSummary; - + // Poll for constructSummary if not available + if (_constructSummary == null) { + _summaryPollTimer = + Timer.periodic(const Duration(milliseconds: 300), (timer) { + final summary = LevelUpManager.instance.constructSummary; + if (summary != null) { + setState(() { + _constructSummary = summary; + }); + timer.cancel(); + } + }); + } final client = Matrix.of(context).client; client.fetchOwnProfile().then((profile) { setState(() { avatarUrl = profile.avatarUrl; }); }); - _controller = AnimationController( duration: _animationDuration, vsync: this, @@ -136,8 +144,18 @@ class _LevelUpPopupContentState extends State _controller.forward(); } - // Use LevelUpManager's constructSummary instead of local _constructSummary + @override + void dispose() { + _summaryPollTimer?.cancel(); + _controller.dispose(); + _confettiController.dispose(); + LevelUpManager.instance.reset(); + stopConfetti(); + super.dispose(); + } + int _getSkillXP(LearningSkillsEnum skill) { + if (_constructSummary == null) return 0; return switch (skill) { LearningSkillsEnum.writing => _constructSummary?.writingConstructScore ?? 0, @@ -151,15 +169,6 @@ class _LevelUpPopupContentState extends State }; } - @override - void dispose() { - _controller.dispose(); - _confettiController.dispose(); - LevelUpManager.instance.reset(); - stopConfetti(); - super.dispose(); - } - @override @override Widget build(BuildContext context) { @@ -335,15 +344,15 @@ class _LevelUpPopupContentState extends State children: [ _buildSkillsTable(context), const SizedBox(height: 8), - if (_constructSummary?.textSummary != null) - Padding( - padding: const EdgeInsets.only(top: 16), - child: Text( - _constructSummary!.textSummary, - textAlign: TextAlign.left, - style: Theme.of(context).textTheme.bodyMedium, - ), + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text( + _constructSummary?.textSummary ?? + L10n.of(context).loadingPleaseWait, + textAlign: TextAlign.left, + style: Theme.of(context).textTheme.bodyMedium, ), + ), //const SizedBox(height: 16), Padding( padding: const EdgeInsets.all(24.0), From 11d3fc29cec30f67f60459278ef3a0047add8dd7 Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Mon, 23 Jun 2025 11:02:42 -0400 Subject: [PATCH 12/32] Add construct numbers to saved construct summary --- .../settings_chat/settings_chat_view.dart | 11 ++ .../get_analytics_controller.dart | 9 +- .../level_up/level_up_banner.dart | 1 + .../level_up/level_up_manager.dart | 134 ++++++++++-------- .../level_up/level_up_popup.dart | 77 +++++++--- lib/pangea/constructs/construct_repo.dart | 11 +- 6 files changed, 161 insertions(+), 82 deletions(-) diff --git a/lib/pages/settings_chat/settings_chat_view.dart b/lib/pages/settings_chat/settings_chat_view.dart index 58eec8d46..e3e5efe5f 100644 --- a/lib/pages/settings_chat/settings_chat_view.dart +++ b/lib/pages/settings_chat/settings_chat_view.dart @@ -2,6 +2,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_banner.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/settings_switch_list_tile.dart'; @@ -28,6 +29,16 @@ class SettingsChatView extends StatelessWidget { child: MaxWidthBody( child: Column( children: [ + ElevatedButton( + onPressed: () { + LevelUpUtil.showLevelUpDialog( + 3, + 2, + context, + ); + }, + child: const Text('Show Level Up Dialog'), + ), // #Pangea // SettingsSwitchListTile.adaptive( // title: L10n.of(context).formattedMessages, diff --git a/lib/pangea/analytics_misc/get_analytics_controller.dart b/lib/pangea/analytics_misc/get_analytics_controller.dart index 3413cc9f5..b328097c3 100644 --- a/lib/pangea/analytics_misc/get_analytics_controller.dart +++ b/lib/pangea/analytics_misc/get_analytics_controller.dart @@ -16,6 +16,7 @@ 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'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:get_storage/get_storage.dart'; import 'package:matrix/matrix.dart'; @@ -478,8 +479,8 @@ class GetAnalyticsController extends BaseController { // generate level up analytics as a construct summary ConstructSummary summary; try { - final int maxXP = constructListModel.calculateXpWithLevel(lowerLevel); - final int minXP = constructListModel.calculateXpWithLevel(upperLevel); + final int maxXP = constructListModel.calculateXpWithLevel(upperLevel); + final int minXP = constructListModel.calculateXpWithLevel(lowerLevel); int diffXP = maxXP - minXP; debugPrint("minXP: $minXP, maxXP: $maxXP, diffXP: $diffXP"); if (diffXP < 0) diffXP = 0; @@ -523,6 +524,10 @@ class GetAnalyticsController extends BaseController { final response = await ConstructRepo.generateConstructSummary(request); summary = response.summary; + summary.levelVocabConstructs = MatrixState + .pangeaController.getAnalytics.constructListModel.vocabLemmas; + summary.levelGrammarConstructs = MatrixState + .pangeaController.getAnalytics.constructListModel.grammarLemmas; } catch (e) { debugPrint("Error generating level up analytics: $e"); ErrorHandler.logError(e: e, data: {'e': e}); diff --git a/lib/pangea/analytics_misc/level_up/level_up_banner.dart b/lib/pangea/analytics_misc/level_up/level_up_banner.dart index ad0b840e4..8dff08370 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_banner.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_banner.dart @@ -101,6 +101,7 @@ class LevelUpBannerState extends State context, widget.level, widget.prevLevel, + true, //value true if testing, false if real data ); _slideController = AnimationController( diff --git a/lib/pangea/analytics_misc/level_up/level_up_manager.dart b/lib/pangea/analytics_misc/level_up/level_up_manager.dart index 806ad7477..e458730a1 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_manager.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_manager.dart @@ -1,13 +1,15 @@ -import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; +import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart'; import 'package:fluffychat/pangea/constructs/construct_repo.dart'; +import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; class LevelUpManager { - // Singleton instance + // Singleton instance so analytics can be generated when level up is initiated, and be ready by the time user clicks on banner static final LevelUpManager instance = LevelUpManager._internal(); - // Private constructor LevelUpManager._internal(); int prevLevel = 0; @@ -26,15 +28,11 @@ class LevelUpManager { bool shouldAutoPopup = false; String? error; - int get vocabCount => - MatrixState.pangeaController.getAnalytics.constructListModel - .unlockedLemmas(ConstructTypeEnum.vocab) - .length; - Future preloadAnalytics( BuildContext context, int level, int prevLevel, + bool test, ) async { this.level = level; this.prevLevel = prevLevel; @@ -47,75 +45,95 @@ class LevelUpManager { nextVocab = MatrixState .pangeaController.getAnalytics.constructListModel.vocabLemmas; - //for now idk how to get these - prevGrammar = nextGrammar < 30 ? 0 : nextGrammar - 30; - - prevVocab = nextVocab < 30 ? 0 : nextVocab - 30; - userL2Code = MatrixState.pangeaController.languageController .activeL2Code() ?.toUpperCase(); - /*for testing, just fetch last level up + // fetch construct summary based on test value + if (test) { + getConstructFromButton(); + } else { + getConstructFromLevelUp(); + } + + final LanguageModel? l2 = + MatrixState.pangeaController.languageController.userL2; + final Room? analyticsRoom = + MatrixState.pangeaController.matrixState.client.analyticsRoomLocal(l2!); + + if (analyticsRoom != null) { + // How to get all summary events in the timeline + final timeline = await analyticsRoom.getTimeline(); + final summaryEvents = timeline.events + .where( + (e) => e.type == PangeaEventTypes.constructSummary, + ) + .map( + (e) => ConstructSummary.fromJson(e.content), + ) + .toList(); + + debugPrint("List of all previous level up summaries: $summaryEvents"); + for (final summary in summaryEvents) { + debugPrint("${summary.toJson()}"); + } + //Find previous summary to get grammar constructs and vocab numbers from + final lastSummary = summaryEvents + .where((summary) => summary.upperLevel == prevLevel) + .toList() + .isNotEmpty + ? summaryEvents + .firstWhere((summary) => summary.upperLevel == prevLevel) + : null; + + //Set grammar and vocab from last level summary, if there is one. Otherwise set to placeholder data + debugPrint("Last construct summary is: ${lastSummary?.toJson()}"); + if (lastSummary != null && + lastSummary.levelVocabConstructs != null && + lastSummary.levelGrammarConstructs != null) { + prevVocab = lastSummary.levelVocabConstructs!; + prevGrammar = lastSummary.levelGrammarConstructs!; + } else { + prevGrammar = nextGrammar < 30 ? 0 : nextGrammar - 30; + prevVocab = nextVocab < 30 ? 0 : nextVocab - 30; + } + } + } + + void getConstructFromButton() { + //for testing, just fetch last level up from saved analytics constructSummary = MatrixState.pangeaController.getAnalytics .getConstructSummaryFromStateEvent(); debugPrint( - "Last saved construct summary: ${constructSummary?.toJson()}", + "Last saved construct summary from analytics controller function: ${constructSummary?.toJson()}", ); + } - final client = MatrixState.pangeaController.matrixState.client; - - final Room? analyticsRoom = client.analyticsRoomLocal( - MatrixState.pangeaController.languageController.userL2!, - ); - - // Get all summary events in the timeline - final timeline = await analyticsRoom!.getTimeline(); - final summaryEvents = timeline.events - .where( - (e) => e.type == PangeaEventTypes.constructSummary, - ) - .map( - (e) => ConstructSummary.fromJson(e.content), - ) - .toList(); - debugPrint("List of previous summaries from timeline: $summaryEvents"); - - for (final summary in summaryEvents) { - debugPrint("Individual summaries from timeline: ${summary.toJson()}"); - } - - */ - - // fetch construct summary for actual app, not while testing since level up isn't true + void getConstructFromLevelUp() async { + //for getting real level up data when leveled up try { constructSummary = await MatrixState.pangeaController.getAnalytics .generateLevelUpAnalytics( - level, prevLevel, + level, ); } catch (e) { error = e.toString(); } - // end of that block - await Future.delayed( - const Duration(seconds: 1), - () => LevelUpManager.instance.printAnalytics(), - ); } - void printAnalytics() { - debugPrint('Level Up Analytics:'); - debugPrint('Current Level: $level'); - debugPrint('Previous Level: $prevLevel'); - debugPrint('Next Grammar: $nextGrammar'); - debugPrint('Next Vocab: $nextVocab'); - if (constructSummary != null) { - debugPrint('Construct Summary: ${constructSummary!.toJson()}'); - } else { - debugPrint('Construct Summary: Not available'); - } - } + // void printAnalytics() { + // debugPrint('Level Up Analytics:'); + // debugPrint('Current Level: $level'); + // debugPrint('Previous Level: $prevLevel'); + // debugPrint('Next Grammar: $nextGrammar'); + // debugPrint('Next Vocab: $nextVocab'); + // if (constructSummary != null) { + // debugPrint('Construct Summary: ${constructSummary!.toJson()}'); + // } else { + // debugPrint('Construct Summary: Not available'); + // } + // } void markPopupSeen() { hasSeenPopup = true; diff --git a/lib/pangea/analytics_misc/level_up/level_up_popup.dart b/lib/pangea/analytics_misc/level_up/level_up_popup.dart index 4055cbe85..1a50ba197 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_popup.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_popup.dart @@ -14,6 +14,7 @@ import 'package:fluffychat/pangea/analytics_summary/progress_bar/level_bar.dart' import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_details.dart'; import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart'; import 'package:fluffychat/pangea/constructs/construct_repo.dart'; +import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:flutter/foundation.dart'; @@ -214,6 +215,8 @@ class _LevelUpPopupContentState extends State fontWeight: FontWeight.bold, color: colorScheme.primary, ); + final username = + Matrix.of(context).client.userID?.split(':').first.substring(1) ?? ''; return Stack( children: [ @@ -230,11 +233,10 @@ class _LevelUpPopupContentState extends State Padding( padding: const EdgeInsets.all(24.0), child: avatarUrl == null - ? MxcImage( - client: Matrix.of(context).client, - fit: BoxFit.cover, - width: 150 * shrinkMultiplier.value, - height: 150 * shrinkMultiplier.value, + ? Avatar( + name: username, + showPresence: false, + size: 150 * shrinkMultiplier.value, ) : ClipOval( child: MxcImage( @@ -310,23 +312,58 @@ class _LevelUpPopupContentState extends State builder: (_, __) => Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Symbols.dictionary, - color: colorScheme.primary, - size: 35, + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "+ ${_endVocab - _startVocab}", + style: const TextStyle( + color: Colors.lightGreen, + fontWeight: FontWeight.bold, + ), + ), + Row( + children: [ + Icon( + Symbols.dictionary, + color: colorScheme.primary, + size: 35, + ), + const SizedBox(width: 8), + Text( + '${vocabAnimation.value}', + style: grammarVocabStyle, + ), + ], + ), + ], ), - const SizedBox(width: 8), - Text('${vocabAnimation.value}', style: grammarVocabStyle), const SizedBox(width: 40), - Icon( - Symbols.toys_and_games, - color: colorScheme.primary, - size: 35, - ), - const SizedBox(width: 8), - Text( - '${grammarAnimation.value}', - style: grammarVocabStyle, + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "+ ${_endGrammar - _startGrammar}", + style: const TextStyle( + color: Colors.lightGreen, + fontWeight: FontWeight.bold, + ), + ), + Row( + children: [ + Icon( + Symbols.toys_and_games, + color: colorScheme.primary, + size: 35, + ), + const SizedBox(width: 8), + Text( + '${grammarAnimation.value}', + style: grammarVocabStyle, + ), + ], + ), + ], ), ], ), diff --git a/lib/pangea/constructs/construct_repo.dart b/lib/pangea/constructs/construct_repo.dart index 6d97ce0af..f6fc19900 100644 --- a/lib/pangea/constructs/construct_repo.dart +++ b/lib/pangea/constructs/construct_repo.dart @@ -1,16 +1,17 @@ import 'dart:convert'; -import 'package:http/http.dart'; - import 'package:fluffychat/pangea/analytics_misc/constructs_model.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/widgets/matrix.dart'; +import 'package:http/http.dart'; class ConstructSummary { final int upperLevel; final int lowerLevel; + int? levelVocabConstructs; + int? levelGrammarConstructs; final String language; final String textSummary; final int writingConstructScore; @@ -21,6 +22,8 @@ class ConstructSummary { ConstructSummary({ required this.upperLevel, required this.lowerLevel, + this.levelVocabConstructs, + this.levelGrammarConstructs, required this.language, required this.textSummary, required this.writingConstructScore, @@ -33,6 +36,8 @@ class ConstructSummary { return { 'upper_level': upperLevel, 'lower_level': lowerLevel, + 'level_grammar_constructs': levelGrammarConstructs, + 'level_vocab_constructs': levelVocabConstructs, 'language': language, 'text_summary': textSummary, 'writing_construct_score': writingConstructScore, @@ -46,6 +51,8 @@ class ConstructSummary { return ConstructSummary( upperLevel: json['upper_level'], lowerLevel: json['lower_level'], + levelGrammarConstructs: json['level_grammar_constructs'], + levelVocabConstructs: json['level_vocab_constructs'], language: json['language'], textSummary: json['text_summary'], writingConstructScore: json['writing_construct_score'], From 74396ed47c55f7bd04c60f0efb4694dc916d9459 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 23 Jun 2025 11:09:27 -0400 Subject: [PATCH 13/32] merge main --- lib/utils/client_manager.dart | 9 +++++++++ pubspec.lock | 16 ++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index 01bcb49c4..27ae647cc 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -10,6 +10,15 @@ import 'package:fluffychat/utils/custom_http_client.dart'; import 'package:fluffychat/utils/custom_image_resizer.dart'; import 'package:fluffychat/utils/init_with_restore.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:matrix/encryption/utils/key_verification.dart'; +import 'package:matrix/matrix.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:universal_html/html.dart' as html; + import 'matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart'; abstract class ClientManager { diff --git a/pubspec.lock b/pubspec.lock index 101510960..17a90ab35 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -38,6 +38,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.3.0" + animated_flip_counter: + dependency: "direct main" + description: + name: animated_flip_counter + sha256: "73f852d84c461c3e4c1ddf320bee334dde8dba89441922ab11a8013be0b2fad1" + url: "https://pub.dev" + source: hosted + version: "0.3.4" animations: dependency: "direct main" description: @@ -334,6 +342,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + confetti: + dependency: "direct main" + description: + name: confetti + sha256: "79376a99648efbc3f23582f5784ced0fe239922bd1a0fb41f582051eba750751" + url: "https://pub.dev" + source: hosted + version: "0.8.0" console: dependency: transitive description: From e52d72cdb4b13ba2d480995f3a0c2a4f5e01acd4 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 23 Jun 2025 12:03:12 -0400 Subject: [PATCH 14/32] reduce number of events fetched for construct summary --- lib/pages/chat/chat.dart | 23 ++++---- .../settings_chat/settings_chat_view.dart | 4 +- .../get_analytics_controller.dart | 58 +++++++++++++------ .../level_up/level_up_banner.dart | 4 +- .../level_up/level_up_manager.dart | 6 +- .../level_up/level_up_popup.dart | 10 ++-- .../level_up/rain_confetti.dart | 6 +- lib/pangea/constructs/construct_repo.dart | 3 +- lib/utils/client_manager.dart | 19 +++--- 9 files changed, 82 insertions(+), 51 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 687890b5c..c028675c0 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -5,9 +5,21 @@ import 'dart:developer'; import 'dart:io'; import 'dart:math'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:collection/collection.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:matrix/matrix.dart'; +import 'package:scroll_to_index/scroll_to_index.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:universal_html/html.dart' as html; + import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; @@ -57,17 +69,6 @@ import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart' import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/share_scaffold_dialog.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:go_router/go_router.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:matrix/matrix.dart'; -import 'package:scroll_to_index/scroll_to_index.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:universal_html/html.dart' as html; - import '../../utils/account_bundles.dart'; import '../../utils/localized_exception_extension.dart'; import 'send_file_dialog.dart'; diff --git a/lib/pages/settings_chat/settings_chat_view.dart b/lib/pages/settings_chat/settings_chat_view.dart index e3e5efe5f..5cac744fa 100644 --- a/lib/pages/settings_chat/settings_chat_view.dart +++ b/lib/pages/settings_chat/settings_chat_view.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; @@ -6,8 +8,6 @@ import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_banner.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/settings_switch_list_tile.dart'; -import 'package:flutter/material.dart'; - import 'settings_chat.dart'; class SettingsChatView extends StatelessWidget { diff --git a/lib/pangea/analytics_misc/get_analytics_controller.dart b/lib/pangea/analytics_misc/get_analytics_controller.dart index b328097c3..5f100b5e0 100644 --- a/lib/pangea/analytics_misc/get_analytics_controller.dart +++ b/lib/pangea/analytics_misc/get_analytics_controller.dart @@ -1,5 +1,11 @@ import 'dart:async'; +import 'package:flutter/material.dart'; + +import 'package:get_storage/get_storage.dart'; +import 'package:matrix/matrix.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_list_model.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; @@ -17,10 +23,6 @@ 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'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:matrix/matrix.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; /// A minimized version of AnalyticsController that get the logged in user's analytics class GetAnalyticsController extends BaseController { @@ -495,23 +497,41 @@ class GetAnalyticsController extends BaseController { } // extract construct use message bodies for analytics - List? constructUseMessageContentBodies = []; + final Map> useEventIds = {}; 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 (use.metadata.roomId == null) continue; + if (use.metadata.eventId == null) continue; + useEventIds[use.metadata.roomId!] ??= {}; + useEventIds[use.metadata.roomId!]!.add(use.metadata.eventId!); } - if (constructUseMessageContentBodies.length != - constructUseOfCurrentLevel.length) { - constructUseMessageContentBodies = null; + + final List constructUseMessageContentBodies = []; + for (final entry in useEventIds.entries) { + final String roomId = entry.key; + final room = _client.getRoomById(roomId); + if (room == null) continue; + final List messageBodies = []; + for (final eventId in entry.value) { + try { + final Event? event = await room.getEventById(eventId); + if (event?.content["body"] is! String) continue; + final String body = event?.content["body"] as String; + if (body.isEmpty) continue; + messageBodies.add(body); + } catch (e, s) { + debugPrint("Error getting event by ID: $e"); + ErrorHandler.logError( + e: e, + s: s, + data: { + 'roomId': roomId, + 'eventId': eventId, + }, + ); + continue; + } + } + constructUseMessageContentBodies.addAll(messageBodies); } final request = ConstructSummaryRequest( diff --git a/lib/pangea/analytics_misc/level_up/level_up_banner.dart b/lib/pangea/analytics_misc/level_up/level_up_banner.dart index 8dff08370..fc0c7fde3 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_banner.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_banner.dart @@ -1,7 +1,10 @@ import 'dart:async'; +import 'package:flutter/material.dart'; + import 'package:audioplayers/audioplayers.dart'; import 'package:cached_network_image/cached_network_image.dart'; + import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; @@ -10,7 +13,6 @@ import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_popup.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/common/utils/overlay.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart'; class LevelUpConstants { static const String starFileName = "star.png"; diff --git a/lib/pangea/analytics_misc/level_up/level_up_manager.dart b/lib/pangea/analytics_misc/level_up/level_up_manager.dart index e458730a1..ed68c12d2 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_manager.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_manager.dart @@ -1,10 +1,12 @@ +import 'package:flutter/material.dart'; + +import 'package:matrix/matrix.dart'; + import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart'; import 'package:fluffychat/pangea/constructs/construct_repo.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart'; -import 'package:matrix/matrix.dart'; class LevelUpManager { // Singleton instance so analytics can be generated when level up is initiated, and be ready by the time user clicks on banner diff --git a/lib/pangea/analytics_misc/level_up/level_up_popup.dart b/lib/pangea/analytics_misc/level_up/level_up_popup.dart index 1a50ba197..f1a11454f 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_popup.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_popup.dart @@ -1,9 +1,15 @@ import 'dart:async'; import 'dart:math'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + import 'package:animated_flip_counter/animated_flip_counter.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:confetti/confetti.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:matrix/matrix_api_lite/generated/model.dart'; + import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart'; @@ -17,10 +23,6 @@ import 'package:fluffychat/pangea/constructs/construct_repo.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:material_symbols_icons/symbols.dart'; -import 'package:matrix/matrix_api_lite/generated/model.dart'; class LevelUpPopup extends StatelessWidget { const LevelUpPopup({ diff --git a/lib/pangea/analytics_misc/level_up/rain_confetti.dart b/lib/pangea/analytics_misc/level_up/rain_confetti.dart index f4d3d7df4..9434ddd65 100644 --- a/lib/pangea/analytics_misc/level_up/rain_confetti.dart +++ b/lib/pangea/analytics_misc/level_up/rain_confetti.dart @@ -1,9 +1,11 @@ import 'dart:math'; -import 'package:confetti/confetti.dart'; -import 'package:fluffychat/config/app_config.dart'; import 'package:flutter/material.dart'; +import 'package:confetti/confetti.dart'; + +import 'package:fluffychat/config/app_config.dart'; + OverlayEntry? _confettiEntry; ConfettiController? _blastController; ConfettiController? _rainController; diff --git a/lib/pangea/constructs/construct_repo.dart b/lib/pangea/constructs/construct_repo.dart index f6fc19900..d847f8168 100644 --- a/lib/pangea/constructs/construct_repo.dart +++ b/lib/pangea/constructs/construct_repo.dart @@ -1,11 +1,12 @@ import 'dart:convert'; +import 'package:http/http.dart'; + import 'package:fluffychat/pangea/analytics_misc/constructs_model.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/widgets/matrix.dart'; -import 'package:http/http.dart'; class ConstructSummary { final int upperLevel; diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index 27ae647cc..974ef75d3 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -1,16 +1,9 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; + import 'package:collection/collection.dart'; import 'package:desktop_notifications/desktop_notifications.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/setting_keys.dart'; -import 'package:fluffychat/pangea/common/constants/model_keys.dart'; -import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; -import 'package:fluffychat/utils/custom_http_client.dart'; -import 'package:fluffychat/utils/custom_image_resizer.dart'; -import 'package:fluffychat/utils/init_with_restore.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:matrix/encryption/utils/key_verification.dart'; @@ -19,6 +12,14 @@ import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:universal_html/html.dart' as html; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; +import 'package:fluffychat/pangea/common/constants/model_keys.dart'; +import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; +import 'package:fluffychat/utils/custom_http_client.dart'; +import 'package:fluffychat/utils/custom_image_resizer.dart'; +import 'package:fluffychat/utils/init_with_restore.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart'; abstract class ClientManager { From 0c244540e1770e85c89d22590819c8d7f171a65a Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Mon, 23 Jun 2025 15:43:59 -0400 Subject: [PATCH 15/32] feat: finish up level-up animation Take out testing button and functions, lower XP needed in staging to level up for test reasons --- .../settings_chat/settings_chat_view.dart | 15 +- .../analytics_misc/construct_list_model.dart | 8 +- .../get_analytics_controller.dart | 11 +- .../level_up/level_up_banner.dart | 5 +- .../level_up/level_up_manager.dart | 43 ++---- .../level_up/level_up_popup.dart | 134 +++++++----------- .../level_up/rain_confetti.dart | 1 - 7 files changed, 69 insertions(+), 148 deletions(-) diff --git a/lib/pages/settings_chat/settings_chat_view.dart b/lib/pages/settings_chat/settings_chat_view.dart index 5cac744fa..58eec8d46 100644 --- a/lib/pages/settings_chat/settings_chat_view.dart +++ b/lib/pages/settings_chat/settings_chat_view.dart @@ -1,13 +1,12 @@ -import 'package:flutter/material.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_banner.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/settings_switch_list_tile.dart'; +import 'package:flutter/material.dart'; + import 'settings_chat.dart'; class SettingsChatView extends StatelessWidget { @@ -29,16 +28,6 @@ class SettingsChatView extends StatelessWidget { child: MaxWidthBody( child: Column( children: [ - ElevatedButton( - onPressed: () { - LevelUpUtil.showLevelUpDialog( - 3, - 2, - context, - ); - }, - child: const Text('Show Level Up Dialog'), - ), // #Pangea // SettingsSwitchListTile.adaptive( // title: L10n.of(context).formattedMessages, diff --git a/lib/pangea/analytics_misc/construct_list_model.dart b/lib/pangea/analytics_misc/construct_list_model.dart index 86a54e963..f2ff3a327 100644 --- a/lib/pangea/analytics_misc/construct_list_model.dart +++ b/lib/pangea/analytics_misc/construct_list_model.dart @@ -1,16 +1,15 @@ import 'dart:math'; -import 'package:flutter/material.dart'; - import 'package:collection/collection.dart'; - import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; +import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; +import 'package:flutter/material.dart'; /// A wrapper around a list of [OneConstructUse]s, used to simplify /// the process of filtering / sorting / displaying the events. @@ -35,7 +34,8 @@ class ConstructListModel { /// [D] is the "compression factor". It determines how quickly /// or slowly the level grows relative to XP - final double D = 1500; + + final double D = Environment.isStagingEnvironment ? 500 : 1500; List unlockedLemmas( ConstructTypeEnum type, { diff --git a/lib/pangea/analytics_misc/get_analytics_controller.dart b/lib/pangea/analytics_misc/get_analytics_controller.dart index 5f100b5e0..0d7c3c183 100644 --- a/lib/pangea/analytics_misc/get_analytics_controller.dart +++ b/lib/pangea/analytics_misc/get_analytics_controller.dart @@ -1,11 +1,5 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - -import 'package:get_storage/get_storage.dart'; -import 'package:matrix/matrix.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; - import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_list_model.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; @@ -23,6 +17,10 @@ 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'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:matrix/matrix.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; /// A minimized version of AnalyticsController that get the logged in user's analytics class GetAnalyticsController extends BaseController { @@ -484,7 +482,6 @@ class GetAnalyticsController extends BaseController { final int maxXP = constructListModel.calculateXpWithLevel(upperLevel); final int minXP = constructListModel.calculateXpWithLevel(lowerLevel); int diffXP = maxXP - minXP; - debugPrint("minXP: $minXP, maxXP: $maxXP, diffXP: $diffXP"); if (diffXP < 0) diffXP = 0; // compute construct use of current level diff --git a/lib/pangea/analytics_misc/level_up/level_up_banner.dart b/lib/pangea/analytics_misc/level_up/level_up_banner.dart index fc0c7fde3..ad0b840e4 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_banner.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_banner.dart @@ -1,10 +1,7 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - import 'package:audioplayers/audioplayers.dart'; import 'package:cached_network_image/cached_network_image.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; @@ -13,6 +10,7 @@ import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_popup.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/common/utils/overlay.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; class LevelUpConstants { static const String starFileName = "star.png"; @@ -103,7 +101,6 @@ class LevelUpBannerState extends State context, widget.level, widget.prevLevel, - true, //value true if testing, false if real data ); _slideController = AnimationController( diff --git a/lib/pangea/analytics_misc/level_up/level_up_manager.dart b/lib/pangea/analytics_misc/level_up/level_up_manager.dart index ed68c12d2..c896b727f 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_manager.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_manager.dart @@ -1,12 +1,10 @@ -import 'package:flutter/material.dart'; - -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart'; import 'package:fluffychat/pangea/constructs/construct_repo.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; class LevelUpManager { // Singleton instance so analytics can be generated when level up is initiated, and be ready by the time user clicks on banner @@ -34,14 +32,13 @@ class LevelUpManager { BuildContext context, int level, int prevLevel, - bool test, ) async { this.level = level; this.prevLevel = prevLevel; + //For on route change behavior, if added in the future shouldAutoPopup = true; - //grammar and vocab nextGrammar = MatrixState .pangeaController.getAnalytics.constructListModel.grammarLemmas; nextVocab = MatrixState @@ -51,12 +48,7 @@ class LevelUpManager { .activeL2Code() ?.toUpperCase(); - // fetch construct summary based on test value - if (test) { - getConstructFromButton(); - } else { - getConstructFromLevelUp(); - } + getConstructFromLevelUp(); final LanguageModel? l2 = MatrixState.pangeaController.languageController.userL2; @@ -75,10 +67,6 @@ class LevelUpManager { ) .toList(); - debugPrint("List of all previous level up summaries: $summaryEvents"); - for (final summary in summaryEvents) { - debugPrint("${summary.toJson()}"); - } //Find previous summary to get grammar constructs and vocab numbers from final lastSummary = summaryEvents .where((summary) => summary.upperLevel == prevLevel) @@ -89,21 +77,20 @@ class LevelUpManager { : null; //Set grammar and vocab from last level summary, if there is one. Otherwise set to placeholder data - debugPrint("Last construct summary is: ${lastSummary?.toJson()}"); if (lastSummary != null && lastSummary.levelVocabConstructs != null && lastSummary.levelGrammarConstructs != null) { prevVocab = lastSummary.levelVocabConstructs!; prevGrammar = lastSummary.levelGrammarConstructs!; } else { - prevGrammar = nextGrammar < 30 ? 0 : nextGrammar - 30; - prevVocab = nextVocab < 30 ? 0 : nextVocab - 30; + prevGrammar = (nextGrammar / prevLevel) as int; + prevVocab = (nextVocab / prevLevel) as int; } } } + //for testing, just fetch last level up from saved analytics void getConstructFromButton() { - //for testing, just fetch last level up from saved analytics constructSummary = MatrixState.pangeaController.getAnalytics .getConstructSummaryFromStateEvent(); debugPrint( @@ -111,8 +98,8 @@ class LevelUpManager { ); } + //for getting real level up data when leveled up void getConstructFromLevelUp() async { - //for getting real level up data when leveled up try { constructSummary = await MatrixState.pangeaController.getAnalytics .generateLevelUpAnalytics( @@ -124,19 +111,6 @@ class LevelUpManager { } } - // void printAnalytics() { - // debugPrint('Level Up Analytics:'); - // debugPrint('Current Level: $level'); - // debugPrint('Previous Level: $prevLevel'); - // debugPrint('Next Grammar: $nextGrammar'); - // debugPrint('Next Vocab: $nextVocab'); - // if (constructSummary != null) { - // debugPrint('Construct Summary: ${constructSummary!.toJson()}'); - // } else { - // debugPrint('Construct Summary: Not available'); - // } - // } - void markPopupSeen() { hasSeenPopup = true; shouldAutoPopup = false; @@ -153,6 +127,5 @@ class LevelUpManager { nextVocab = 0; constructSummary = null; error = null; - // Reset any other state if necessary } } diff --git a/lib/pangea/analytics_misc/level_up/level_up_popup.dart b/lib/pangea/analytics_misc/level_up/level_up_popup.dart index f1a11454f..8d74dd468 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_popup.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_popup.dart @@ -1,15 +1,9 @@ import 'dart:async'; import 'dart:math'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - import 'package:animated_flip_counter/animated_flip_counter.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:confetti/confetti.dart'; -import 'package:material_symbols_icons/symbols.dart'; -import 'package:matrix/matrix_api_lite/generated/model.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart'; @@ -23,6 +17,10 @@ import 'package:fluffychat/pangea/constructs/construct_repo.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:matrix/matrix_api_lite/generated/model.dart'; class LevelUpPopup extends StatelessWidget { const LevelUpPopup({ @@ -74,24 +72,21 @@ class _LevelUpPopupContentState extends State with SingleTickerProviderStateMixin { late int _endGrammar; late int _endVocab; - late final AnimationController _controller; - - Uri? avatarUrl; - late final Future profile; - - late final ConfettiController _confettiController; - - int displayedLevel = -1; - bool _hasBlastedConfetti = false; - final int _startGrammar = LevelUpManager.instance.prevGrammar; final int _startVocab = LevelUpManager.instance.prevVocab; - late ConstructSummary? _constructSummary; Timer? _summaryPollTimer; final String? _error = LevelUpManager.instance.error; String language = LevelUpManager.instance.userL2Code ?? "N/A"; - static const Duration _animationDuration = Duration(seconds: 5); + late final AnimationController _controller; + late final ConfettiController _confettiController; + bool _hasBlastedConfetti = false; + final Duration _animationDuration = const Duration(seconds: 5); + + Uri? avatarUrl; + late final Future profile; + int displayedLevel = -1; + late ConstructSummary? _constructSummary; @override void initState() { @@ -269,7 +264,7 @@ class _LevelUpPopupContentState extends State builder: (context, constraints) { return LevelBar( details: const LevelBarDetails( - fillColor: Colors.green, + fillColor: AppConfig.goldLight, currentPoints: 0, widthMultiplier: 1, ), @@ -392,7 +387,6 @@ class _LevelUpPopupContentState extends State style: Theme.of(context).textTheme.bodyMedium, ), ), - //const SizedBox(height: 16), Padding( padding: const EdgeInsets.all(24.0), child: CachedNetworkImage( @@ -419,41 +413,41 @@ class _LevelUpPopupContentState extends State ), ), // 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, - ), - ], - ), - ), + // 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, + // ), + // ], + // ), + // ), ], ), ), @@ -467,7 +461,7 @@ class _LevelUpPopupContentState extends State .toList(); const itemsPerRow = 4; - // chunk into rows of up to 3 + // chunk into rows of up to 4 final rows = >[ for (var i = 0; i < visibleSkills.length; i += itemsPerRow) visibleSkills.sublist( @@ -522,32 +516,4 @@ class _LevelUpPopupContentState extends State }).toList(), ); } - - Path drawStar(Size size) { - // Method to convert degrees to radians - double degToRad(double deg) => deg * (pi / 180.0); - - const numberOfPoints = 5; - final halfWidth = size.width / 2; - final externalRadius = halfWidth; - final internalRadius = halfWidth / 2.5; - final degreesPerStep = degToRad(360 / numberOfPoints); - final halfDegreesPerStep = degreesPerStep / 2; - final path = Path(); - final fullAngle = degToRad(360); - path.moveTo(size.width, halfWidth); - - for (double step = 0; step < fullAngle; step += degreesPerStep) { - path.lineTo( - halfWidth + externalRadius * cos(step), - halfWidth + externalRadius * sin(step), - ); - path.lineTo( - halfWidth + internalRadius * cos(step + halfDegreesPerStep), - halfWidth + internalRadius * sin(step + halfDegreesPerStep), - ); - } - path.close(); - return path; - } } diff --git a/lib/pangea/analytics_misc/level_up/rain_confetti.dart b/lib/pangea/analytics_misc/level_up/rain_confetti.dart index 9434ddd65..13e43aca3 100644 --- a/lib/pangea/analytics_misc/level_up/rain_confetti.dart +++ b/lib/pangea/analytics_misc/level_up/rain_confetti.dart @@ -20,7 +20,6 @@ void rainConfetti(BuildContext context) { _rainController!.play(); final screenWidth = MediaQuery.of(context).size.width; - final screenHeight = MediaQuery.of(context).size.height; final isSmallScreen = screenWidth < 600; final count = isSmallScreen ? 2 : 5; final spacing = screenWidth / (count + 1); From 0f07eee762b6c140d70936eaea6f0c8faed194d4 Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Mon, 23 Jun 2025 15:51:51 -0400 Subject: [PATCH 16/32] chore: sorted imports --- lib/pages/settings_chat/settings_chat_view.dart | 4 ++-- lib/pangea/analytics_misc/construct_list_model.dart | 4 +++- .../analytics_misc/get_analytics_controller.dart | 10 ++++++---- .../analytics_misc/level_up/level_up_banner.dart | 4 +++- .../analytics_misc/level_up/level_up_manager.dart | 6 ++++-- lib/pangea/analytics_misc/level_up/level_up_popup.dart | 10 ++++++---- 6 files changed, 24 insertions(+), 14 deletions(-) diff --git a/lib/pages/settings_chat/settings_chat_view.dart b/lib/pages/settings_chat/settings_chat_view.dart index 58eec8d46..ca75754bd 100644 --- a/lib/pages/settings_chat/settings_chat_view.dart +++ b/lib/pages/settings_chat/settings_chat_view.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; @@ -5,8 +7,6 @@ import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/settings_switch_list_tile.dart'; -import 'package:flutter/material.dart'; - import 'settings_chat.dart'; class SettingsChatView extends StatelessWidget { diff --git a/lib/pangea/analytics_misc/construct_list_model.dart b/lib/pangea/analytics_misc/construct_list_model.dart index f2ff3a327..a7326e26f 100644 --- a/lib/pangea/analytics_misc/construct_list_model.dart +++ b/lib/pangea/analytics_misc/construct_list_model.dart @@ -1,6 +1,9 @@ import 'dart:math'; +import 'package:flutter/material.dart'; + import 'package:collection/collection.dart'; + import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart'; @@ -9,7 +12,6 @@ import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; -import 'package:flutter/material.dart'; /// A wrapper around a list of [OneConstructUse]s, used to simplify /// the process of filtering / sorting / displaying the events. diff --git a/lib/pangea/analytics_misc/get_analytics_controller.dart b/lib/pangea/analytics_misc/get_analytics_controller.dart index 0d7c3c183..5d861b089 100644 --- a/lib/pangea/analytics_misc/get_analytics_controller.dart +++ b/lib/pangea/analytics_misc/get_analytics_controller.dart @@ -1,5 +1,11 @@ import 'dart:async'; +import 'package:flutter/material.dart'; + +import 'package:get_storage/get_storage.dart'; +import 'package:matrix/matrix.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_list_model.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; @@ -17,10 +23,6 @@ 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'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:matrix/matrix.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; /// A minimized version of AnalyticsController that get the logged in user's analytics class GetAnalyticsController extends BaseController { diff --git a/lib/pangea/analytics_misc/level_up/level_up_banner.dart b/lib/pangea/analytics_misc/level_up/level_up_banner.dart index ad0b840e4..71804a5e5 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_banner.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_banner.dart @@ -1,7 +1,10 @@ import 'dart:async'; +import 'package:flutter/material.dart'; + import 'package:audioplayers/audioplayers.dart'; import 'package:cached_network_image/cached_network_image.dart'; + import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; @@ -10,7 +13,6 @@ import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_popup.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/common/utils/overlay.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart'; class LevelUpConstants { static const String starFileName = "star.png"; diff --git a/lib/pangea/analytics_misc/level_up/level_up_manager.dart b/lib/pangea/analytics_misc/level_up/level_up_manager.dart index c896b727f..7716d4165 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_manager.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_manager.dart @@ -1,10 +1,12 @@ +import 'package:flutter/material.dart'; + +import 'package:matrix/matrix.dart'; + import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart'; import 'package:fluffychat/pangea/constructs/construct_repo.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart'; -import 'package:matrix/matrix.dart'; class LevelUpManager { // Singleton instance so analytics can be generated when level up is initiated, and be ready by the time user clicks on banner diff --git a/lib/pangea/analytics_misc/level_up/level_up_popup.dart b/lib/pangea/analytics_misc/level_up/level_up_popup.dart index 8d74dd468..c7aa2fb3b 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_popup.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_popup.dart @@ -1,9 +1,15 @@ import 'dart:async'; import 'dart:math'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + import 'package:animated_flip_counter/animated_flip_counter.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:confetti/confetti.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:matrix/matrix_api_lite/generated/model.dart'; + import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart'; @@ -17,10 +23,6 @@ import 'package:fluffychat/pangea/constructs/construct_repo.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:material_symbols_icons/symbols.dart'; -import 'package:matrix/matrix_api_lite/generated/model.dart'; class LevelUpPopup extends StatelessWidget { const LevelUpPopup({ From 26ff08cfefb83918a6262acbf526a52871c3358b Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 24 Jun 2025 13:56:51 -0400 Subject: [PATCH 17/32] feat: redirect from mobile browser to app or appstore --- android/app/src/main/AndroidManifest.xml | 13 +++++++++++ ios/Runner/Info.plist | 10 +++++++++ ios/Runner/Runner.entitlements | 28 +++++++++++------------- web/index.html | 27 +++++++++++++++++++++++ 4 files changed, 63 insertions(+), 15 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a4058486b..211d34df2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -108,6 +108,19 @@ + + + + + + + + + + + ???? CFBundleURLTypes + + CFBundleURLSchemes + + pangea + + CFBundleURLName + com.talktolearn.chat + CFBundleTypeRole Editor @@ -113,5 +121,7 @@ io.flutter.embedded_views_preview + FlutterDeepLinkingEnabled + diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 2b2a88dd1..91e1a0719 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -1,18 +1,16 @@ - - aps-environment - development - com.apple.developer.associated-domains - - applinks:example.com - - com.apple.security.application-groups - - - group.com.talktolearn.chat - - - - \ No newline at end of file + + aps-environment + development + com.apple.developer.associated-domains + + applinks:app.pangea.chat + + com.apple.security.application-groups + + group.com.talktolearn.chat + + + diff --git a/web/index.html b/web/index.html index 1a3730e5e..cdb551c4d 100644 --- a/web/index.html +++ b/web/index.html @@ -70,6 +70,33 @@ }); }); + + + + +