All real data, confetti tweaks, remove test button, timing and animation tweaks, added copy to arb file

This commit is contained in:
avashilling 2025-06-13 16:29:49 -04:00
parent d49d08f67b
commit fe22e2bcd2
5 changed files with 193 additions and 190 deletions

View file

@ -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!"
}

View file

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

View file

@ -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<void> _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<LevelUpBanner>
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<LevelUpBanner>
_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<LevelUpBanner>
Future<void> _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<LevelUpBanner>
),
),
),
// Optional staging-only dropdown icon
SizedBox(
width: constraints.maxWidth >= 600 ? 120.0 : 65.0,
child: Row(

View file

@ -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<void> 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<void> 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
}
}

View file

@ -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<LevelUpBarAnimation> createState() => _LevelUpBarAnimationState();
State<LevelUpPopupContent> createState() => _LevelUpPopupContentState();
}
class _LevelUpBarAnimationState extends State<LevelUpBarAnimation>
class _LevelUpPopupContentState extends State<LevelUpPopupContent>
with SingleTickerProviderStateMixin {
late int _endGrammar;
late int _endVocab;
late final AnimationController _controller;
late final Animation<double> _progressAnimation;
late final Animation<int> _vocabAnimation;
late final Animation<int> _grammarAnimation;
late final Animation<double> _skillsOpacity;
late final Animation<double> _shrinkMultiplier;
Uri? avatarUrl;
late final Future<Profile> 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<LevelUpBarAnimation>
}
});
_progressAnimation = Tween<double>(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<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.7, 1.0, curve: Curves.easeIn),
),
);
_shrinkMultiplier = Tween<double>(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<void> _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<LevelUpBarAnimation>
void dispose() {
_controller.dispose();
_confettiController.dispose();
LevelUpManager.instance.reset();
super.dispose();
}
@override
@override
Widget build(BuildContext context) {
final Animation<double> progressAnimation =
Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(parent: _controller, curve: const Interval(0.0, 0.5)),
);
final Animation<int> vocabAnimation =
IntTween(begin: _startVocab, end: _endVocab).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.5, curve: Curves.easeInOutQuad),
),
);
final Animation<int> grammarAnimation =
IntTween(begin: _startGrammar, end: _endGrammar).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.5, curve: Curves.easeInOutQuad),
),
);
final Animation<double> skillsOpacity =
Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.7, 1.0, curve: Curves.easeIn),
),
);
final Animation<double> shrinkMultiplier =
Tween<double>(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<LevelUpBarAnimation>
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<LevelUpBarAnimation>
),
// 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<LevelUpBarAnimation>
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<LevelUpBarAnimation>
),
const SizedBox(width: 8),
Text(
'${_grammarAnimation.value}',
'${grammarAnimation.value}',
style: grammarVocabStyle,
),
],
@ -323,9 +305,9 @@ class _LevelUpBarAnimationState extends State<LevelUpBarAnimation>
// 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<LevelUpBarAnimation>
.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,