diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 667e5afe9..188fcf68b 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -389,9 +389,8 @@ class ChatController extends State (update) { if (update['level_up'] != null) { LevelUpUtil.showLevelUpDialog( - update['level_up'], - update['analytics_room_id'], - update['construct_summary'], + update['upper_level'], + update['lower_level'], context, ); } else if (update['unlocked_constructs'] != null) { diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index d1121c558..adceff411 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -43,7 +43,6 @@ import '../../widgets/matrix.dart'; import 'package:fluffychat/utils/tor_stub.dart' if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart'; - enum PopupMenuAction { settings, invite, diff --git a/lib/pangea/analytics_misc/get_analytics_controller.dart b/lib/pangea/analytics_misc/get_analytics_controller.dart index 8c633b9dc..3b473fb68 100644 --- a/lib/pangea/analytics_misc/get_analytics_controller.dart +++ b/lib/pangea/analytics_misc/get_analytics_controller.dart @@ -202,15 +202,11 @@ class GetAnalyticsController extends BaseController { ), ); - Future _onLevelUp(final int lowerLevel, final int upperLevel) async { - final result = await _generateLevelUpAnalyticsAndSaveToStateEvent( - lowerLevel, - upperLevel, - ); + void _onLevelUp(final int lowerLevel, final int upperLevel) { setState({ 'level_up': constructListModel.level, - 'analytics_room_id': _client.analyticsRoomLocal(_l2!)?.id, - "construct_summary": result, + 'upper_level': upperLevel, + 'lower_level': lowerLevel, }); } @@ -465,7 +461,7 @@ class GetAnalyticsController extends BaseController { } } - Future _generateLevelUpAnalyticsAndSaveToStateEvent( + Future generateLevelUpAnalytics( final int lowerLevel, final int upperLevel, ) async { diff --git a/lib/pangea/analytics_misc/level_up.dart b/lib/pangea/analytics_misc/level_up.dart index e3973b584..89987ad05 100644 --- a/lib/pangea/analytics_misc/level_up.dart +++ b/lib/pangea/analytics_misc/level_up.dart @@ -7,8 +7,10 @@ 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/common/utils/overlay.dart'; import 'package:fluffychat/pangea/constructs/construct_repo.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -20,8 +22,7 @@ class LevelUpConstants { class LevelUpUtil { static Future showLevelUpDialog( int level, - String? analyticsRoomId, - ConstructSummary? constructSummary, + int prevLevel, BuildContext context, ) async { final player = AudioPlayer(); @@ -33,51 +34,42 @@ class LevelUpUtil { await Future.delayed(const Duration(milliseconds: 100)); } - player.play( - UrlSource( - "${AppConfig.assetsBaseURL}/${AnalyticsConstants.levelUpAudioFileName}", - ), - ); + player + .play( + UrlSource( + "${AppConfig.assetsBaseURL}/${AnalyticsConstants.levelUpAudioFileName}", + ), + ) + .then( + (_) => Future.delayed( + const Duration(seconds: 2), + () => player.dispose(), + ), + ); - final ValueNotifier showDetailsClicked = ValueNotifier(false); - - late final OverlayEntry overlayEntry; - overlayEntry = OverlayEntry( - builder: (context) => LevelUpBanner( + OverlayUtil.showOverlay( + overlayKey: "level_up_notification", + context: context, + child: LevelUpBanner( level: level, - constructSummary: constructSummary, - onDetailsClicked: () { - showDetailsClicked.value = true; - }, - onOverlayExit: () { - overlayEntry.remove(); - player.dispose(); - }, + prevLevel: prevLevel, ), + transformTargetId: '', + position: OverlayPositionEnum.top, + backDropToDismiss: false, + closePrevOverlay: false, + canPop: false, ); - - Overlay.of(context).insert(overlayEntry); - - Future.delayed(const Duration(seconds: 5), () { - if (!showDetailsClicked.value) { - overlayEntry.remove(); - player.dispose(); - } - }); } } class LevelUpBanner extends StatefulWidget { final int level; - final ConstructSummary? constructSummary; - final VoidCallback onDetailsClicked; - final VoidCallback onOverlayExit; + final int prevLevel; const LevelUpBanner({ required this.level, - this.constructSummary, - required this.onDetailsClicked, - required this.onOverlayExit, + required this.prevLevel, super.key, }); @@ -86,17 +78,28 @@ class LevelUpBanner extends StatefulWidget { } class LevelUpBannerState extends State - with SingleTickerProviderStateMixin { - late AnimationController _controller; + with TickerProviderStateMixin { + late AnimationController _slideController; late Animation _slideAnimation; + + late AnimationController _sizeController; + late Animation _sizeAnimation; + bool _showDetails = false; + bool _showedDetails = false; + + ConstructSummary? _constructSummary; + bool _loading = true; + String? _error; @override void initState() { super.initState(); - _controller = AnimationController( + _setConstructSummary(); + + _slideController = AnimationController( vsync: this, - duration: const Duration(milliseconds: 500), + duration: FluffyThemes.animationDuration, ); _slideAnimation = Tween( @@ -104,322 +107,417 @@ class LevelUpBannerState extends State end: Offset.zero, ).animate( CurvedAnimation( - parent: _controller, + parent: _slideController, curve: Curves.easeOut, ), ); - _controller.forward(); + _sizeController = AnimationController( + vsync: this, + duration: FluffyThemes.animationDuration, + ); + + _sizeAnimation = Tween( + begin: 0, + end: 1, + ).animate( + CurvedAnimation( + parent: _sizeController, + curve: Curves.easeOut, + ), + ); + + _slideController.forward(); + + MatrixState.pangeaController.getAnalytics + .generateLevelUpAnalytics( + widget.level, + widget.prevLevel, + ) + .then((summary) { + if (mounted) { + setState(() { + _constructSummary = summary; + }); + } + }); + + Future.delayed(const Duration(seconds: 15), () async { + if (mounted && !_showedDetails) _close(); + }); } @override void dispose() { - _controller.dispose(); + _slideController.dispose(); + _sizeController.dispose(); super.dispose(); } + Future _setConstructSummary() async { + try { + _constructSummary = await MatrixState.pangeaController.getAnalytics + .generateLevelUpAnalytics( + widget.level, + widget.prevLevel, + ); + } catch (e) { + _error = e.toString(); + } finally { + if (mounted) { + setState(() => _loading = false); + } + } + } + + Future _close() async { + await _slideController.reverse(); + MatrixState.pAnyState.closeOverlay("level_up_notification"); + } + int _skillsPoints(LearningSkillsEnum skill) { switch (skill) { case LearningSkillsEnum.writing: - return widget.constructSummary?.writingConstructScore ?? 0; + return _constructSummary?.writingConstructScore ?? 0; case LearningSkillsEnum.reading: - return widget.constructSummary?.readingConstructScore ?? 0; + return _constructSummary?.readingConstructScore ?? 0; case LearningSkillsEnum.speaking: - return widget.constructSummary?.speakingConstructScore ?? 0; + return _constructSummary?.speakingConstructScore ?? 0; case LearningSkillsEnum.hearing: - return widget.constructSummary?.hearingConstructScore ?? 0; + return _constructSummary?.hearingConstructScore ?? 0; default: return 0; } } + Future _toggleDetails() async { + if (mounted) { + setState(() { + _showDetails = !_showDetails; + if (_showDetails && !_showedDetails) { + _showedDetails = true; + } + }); + + await (_showDetails + ? _sizeController.forward() + : _sizeController.reverse()); + + if (!_showDetails) { + await Future.delayed( + const Duration(milliseconds: 300), + () async { + if (!mounted) return; + _close(); + }, + ); + } + } + } + @override Widget build(BuildContext context) { - return Stack( - children: [ - if (_showDetails) - GestureDetector( - onTap: () { - setState(() { - _showDetails = false; - }); - widget.onOverlayExit(); - }, - child: Container( - color: Colors.black.withAlpha(180), - ), - ), - SlideTransition( - position: _slideAnimation, - child: Align( - alignment: Alignment.topCenter, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.5, - maxHeight: MediaQuery.of(context).size.height * 0.75, - ), - margin: const EdgeInsets.only(top: 16), - decoration: BoxDecoration( - color: widget.level > 10 - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(8), - ), - padding: const EdgeInsets.symmetric( - vertical: 16, - horizontal: 24, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - RichText( - text: TextSpan( - children: [ - TextSpan( - text: L10n.of(context).congratulationsOnReaching, - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), - ), - TextSpan( - text: "${L10n.of(context).level} ", - style: const TextStyle( - color: AppConfig.gold, - fontSize: 18, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), - ), - TextSpan( - text: "${widget.level} ", - style: const TextStyle( - color: AppConfig.gold, - fontSize: 18, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), - ), - WidgetSpan( - child: CachedNetworkImage( - imageUrl: - "${AppConfig.assetsBaseURL}/${LevelUpConstants.starFileName}", - height: 24, - width: 24, - ), - ), - ], - ), - ), - ElevatedButton( - onPressed: () { - setState(() { - _showDetails = !_showDetails; - }); - widget.onDetailsClicked(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: Theme.of(context) - .colorScheme - .surfaceContainerHighest, - ), - child: Row( - children: [ - Text( - "${L10n.of(context).seeDetails} ", - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - Container( - decoration: const BoxDecoration( - color: AppConfig.gold, - shape: BoxShape.circle, - ), - padding: const EdgeInsets.all( - 4.0, - ), - child: const Icon( - Icons.keyboard_arrow_down_rounded, - size: 20, - color: Colors.white, - ), - ), - ], - ), - ), - ], - ), + final style = FluffyThemes.isColumnMode(context) + ? Theme.of(context).textTheme.titleLarge?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ) + : Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ); + + return Material( + color: Colors.transparent, + child: Stack( + children: [ + SlideTransition( + position: _slideAnimation, + child: Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 600.0, ), - AnimatedSize( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - child: _showDetails - ? Container( - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.5, - maxHeight: - MediaQuery.of(context).size.height * 0.75, - ), - margin: const EdgeInsets.only( - top: 16, - ), - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(8), - ), - padding: const EdgeInsets.all(16), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 24.0, - children: [ - Table( - columnWidths: const { - 0: IntrinsicColumnWidth(), - 1: FlexColumnWidth(), - 2: IntrinsicColumnWidth(), - }, - defaultVerticalAlignment: - TableCellVerticalAlignment.middle, - children: [ - ...LearningSkillsEnum.values - .where( - (v) => - v.isVisible && _skillsPoints(v) > -1, - ) - .map((skill) { - return TableRow( - children: [ - Padding( - padding: const EdgeInsets.symmetric( - vertical: 9.0, - horizontal: 18.0, - ), - child: Icon( - skill.icon, - size: 25, - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 9.0, - horizontal: 18.0, - ), - child: Text( - skill.tooltip(context), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - textAlign: TextAlign.center, - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 9.0, - horizontal: 18.0, - ), - child: Text( - "+ ${_skillsPoints(skill)} XP", - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - textAlign: TextAlign.center, - ), - ), - ], - ); - }), - ], - ), - CachedNetworkImage( - imageUrl: - "${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}", - width: 400, - fit: BoxFit.cover, - ), - if (widget.constructSummary?.textSummary != - null) - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .secondaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - widget.constructSummary!.textSummary, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - textAlign: TextAlign.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + margin: const EdgeInsets.only( + top: 16, + ), + decoration: BoxDecoration( + color: widget.level > 10 + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 24, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: 8.0, + children: [ + Flexible( + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: L10n.of(context) + .congratulationsOnReaching, + style: style, + ), + TextSpan( + text: "${L10n.of(context).level} ", + style: style, + ), + TextSpan( + text: "${widget.level} ", + style: style, + ), + WidgetSpan( + child: CachedNetworkImage( + imageUrl: + "${AppConfig.assetsBaseURL}/${LevelUpConstants.starFileName}", + height: 24, + width: 24, ), ), - const SizedBox( - height: 24, - ), - // Share button, currently no functionality - // ElevatedButton( - // onPressed: () { - // // Add share functionality - // }, - // style: ElevatedButton.styleFrom( - // backgroundColor: Colors.white, - // foregroundColor: Colors.black, - // padding: const EdgeInsets.symmetric( - // vertical: 12, - // horizontal: 24, - // ), - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(8), - // ), - // ), - // child: const Row( - // mainAxisSize: MainAxisSize - // .min, - // children: [ - // Text( - // "Share with Friends", - // style: TextStyle( - // fontSize: 16, - // fontWeight: FontWeight.bold, - // ), - // ), - // SizedBox( - // width: 8, - // ), - // Icon( - // Icons.ios_share, - // size: 20, - // ), - // ), - // ), - // ), - ], + ], + ), ), ), - ) - : const SizedBox.shrink(), + AnimatedSize( + duration: FluffyThemes.animationDuration, + child: _error == null + ? ElevatedButton( + onPressed: _error != null + ? null + : _constructSummary != null + ? _toggleDetails + : () {}, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Theme.of(context) + .colorScheme + .surfaceContainerHighest, + ), + child: Row( + children: [ + Text( + "${L10n.of(context).seeDetails} ", + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Container( + decoration: const BoxDecoration( + color: AppConfig.gold, + shape: BoxShape.circle, + ), + padding: const EdgeInsets.all( + 4.0, + ), + child: _loading + ? const CircularProgressIndicator + .adaptive() + : Icon( + _showDetails + ? Icons + .keyboard_arrow_down_rounded + : Icons + .keyboard_arrow_up_rounded, + size: 20, + color: Colors.white, + ), + ), + ], + ), + ) + : Row( + children: [ + Tooltip( + message: L10n.of(context) + .oopsSomethingWentWrong, + child: Icon( + Icons.error, + color: Theme.of(context) + .colorScheme + .error, + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: _close, + ), + ], + ), + ), + ], + ), + ), + SizeTransition( + sizeFactor: _sizeAnimation, + child: Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.75, + ), + margin: const EdgeInsets.only( + top: 16, + ), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.all(16), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 24.0, + children: [ + Table( + columnWidths: const { + 0: IntrinsicColumnWidth(), + 1: FlexColumnWidth(), + 2: IntrinsicColumnWidth(), + }, + defaultVerticalAlignment: + TableCellVerticalAlignment.middle, + children: [ + ...LearningSkillsEnum.values + .where( + (v) => v.isVisible && _skillsPoints(v) > -1, + ) + .map((skill) { + return TableRow( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 9.0, + horizontal: 18.0, + ), + child: Icon( + skill.icon, + size: 25, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 9.0, + horizontal: 18.0, + ), + child: Text( + skill.tooltip(context), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + textAlign: TextAlign.center, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 9.0, + horizontal: 18.0, + ), + child: Text( + "+ ${_skillsPoints(skill)} XP", + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + textAlign: TextAlign.center, + ), + ), + ], + ); + }), + ], + ), + CachedNetworkImage( + imageUrl: + "${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}", + width: 400, + fit: BoxFit.cover, + ), + if (_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: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox( + height: 24, + ), + // Share button, currently no functionality + // ElevatedButton( + // onPressed: () { + // // Add share functionality + // }, + // style: ElevatedButton.styleFrom( + // backgroundColor: Colors.white, + // foregroundColor: Colors.black, + // padding: const EdgeInsets.symmetric( + // vertical: 12, + // horizontal: 24, + // ), + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(8), + // ), + // ), + // child: const Row( + // mainAxisSize: MainAxisSize + // .min, + // children: [ + // Text( + // "Share with Friends", + // style: TextStyle( + // fontSize: 16, + // fontWeight: FontWeight.bold, + // ), + // ), + // SizedBox( + // width: 8, + // ), + // Icon( + // Icons.ios_share, + // size: 20, + // ), + // ), + // ), + // ), + ], + ), + ), + ), + ), + ], ), - ], + ), ), ), - ), - ], + ], + ), ); } }