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