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] 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,