style and functionality changes to level up notification (#2444)

* style and functionality changes to level up notification

* generated

* chore: return construct summary directly from function instead of waiting for state event to be sent

* generated

* XP animation bug, asking wilson to take a look

* updated XP attributes but still facing XP retrieval bug

* generated

* Added new DinoBot image

* updated dinoBot image, added padding on sides to table row, fixed duplicate variable naming error

* generated

* chore: some updates to simplify level up widget

* chore: remove dino asset from pubspec.yaml

* chore: revert testing changes to analytics controller

* See details categories do not display unless XP gained above threshold

* generated

* chore: update icons in construct update popup above messages

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: ggurdin <ggurdin@gmail.com>
This commit is contained in:
Sofanyas Genene 2025-04-29 09:09:03 -04:00 committed by GitHub
parent 4b56b8adb7
commit 1e20d5fb2c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 562 additions and 219 deletions

View file

@ -4596,9 +4596,9 @@
"meaningSectionHeader": "Meaning:",
"formSectionHeader": "Forms used in chats:",
"noEmojiSelectedTooltip": "No emoji selected",
"writingExercisesTooltip": "Writing activities",
"listeningExercisesTooltip": "Listening activities",
"readingExercisesTooltip": "Reading activities",
"writingExercisesTooltip": "Writing practice",
"listeningExercisesTooltip": "Listening practice",
"readingExercisesTooltip": "Reading practice",
"meaningNotFound": "Meaning could not be found.",
"formsNotFound": "Forms could not be found.",
"chooseBaseForm": "Choose the base form",
@ -4875,5 +4875,7 @@
"languageLevelC1Desc": "I can express ideas fluently and spontaneously without much struggle and understand a wide range of demanding texts.",
"languageLevelC2Desc": "I can understand virtually everything heard or read and express myself fluently and precisely.",
"newVocab": "New vocab",
"newGrammar": "New grammar concepts"
"newGrammar": "New grammar concepts",
"congratulationsOnReaching": "Congratulations on reaching ",
"seeDetails": "See Details"
}

View file

@ -391,7 +391,6 @@ class ChatController extends State<ChatPageWithRoom>
LevelUpUtil.showLevelUpDialog(
update['level_up'],
update['analytics_room_id'],
update["construct_summary_state_event_id"],
update['construct_summary'],
context,
);

View file

@ -1,8 +1,6 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/analytics_details_popup/lemma_usage_dots.dart';
@ -73,27 +71,16 @@ class AnalyticsDetailsViewContent extends StatelessWidget {
child: Column(
children: [
LemmaUseExampleMessages(construct: construct),
// Writing exercise section
LemmaUsageDots(
construct: construct,
category: LearningSkillsEnum.writing,
tooltip: L10n.of(context).writingExercisesTooltip,
icon: Symbols.edit_square,
),
// Listening exercise section
LemmaUsageDots(
construct: construct,
category: LearningSkillsEnum.hearing,
tooltip: L10n.of(context).listeningExercisesTooltip,
icon: Icons.volume_up,
),
// Reading exercise section
LemmaUsageDots(
construct: construct,
category: LearningSkillsEnum.reading,
tooltip: L10n.of(context).readingExercisesTooltip,
icon: Symbols.two_pager,
),
...LearningSkillsEnum.values
.where((v) => v.isVisible)
.map((skill) {
return LemmaUsageDots(
construct: construct,
category: skill,
tooltip: skill.tooltip(context),
icon: skill.icon,
);
}),
],
),
),

View file

@ -17,6 +17,8 @@ import 'package:fluffychat/pangea/common/controllers/base_controller.dart';
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/constructs/construct_repo.dart';
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';
@ -199,15 +201,14 @@ class GetAnalyticsController extends BaseController {
);
Future<void> _onLevelUp(final int lowerLevel, final int upperLevel) async {
// final result = await _generateLevelUpAnalyticsAndSaveToStateEvent(
// lowerLevel,
// upperLevel,
// );
final result = await _generateLevelUpAnalyticsAndSaveToStateEvent(
lowerLevel,
upperLevel,
);
setState({
'level_up': constructListModel.level,
// 'analytics_room_id': _client.analyticsRoomLocal(_l2!)?.id,
// "construct_summary_state_event_id": result?.stateEventId,
// "construct_summary": result?.summary,
'analytics_room_id': _client.analyticsRoomLocal(_l2!)?.id,
"construct_summary": result,
});
}
@ -398,6 +399,19 @@ class GetAnalyticsController extends BaseController {
_cache.add(entry);
}
Future<String> _saveConstructSummaryResponseToStateEvent(
final ConstructSummary summary,
) async {
final Room? analyticsRoom = _client.analyticsRoomLocal(_l2!);
final stateEventId = await _client.setRoomStateWithKey(
analyticsRoom!.id,
PangeaEventTypes.constructSummary,
'',
summary.toJson(),
);
return stateEventId;
}
int newConstructCount(
List<OneConstructUse> newConstructs,
ConstructTypeEnum type,
@ -434,76 +448,97 @@ class GetAnalyticsController extends BaseController {
// int diffXP = maxXP - minXP;
// if (diffXP < 0) diffXP = 0;
// // compute construct use of current level
// final List<OneConstructUse> constructUseOfCurrentLevel = [];
// int score = 0;
// for (final use in constructListModel.uses) {
// constructUseOfCurrentLevel.add(use);
// score += use.pointValue;
// if (score >= diffXP) break;
// }
Future<ConstructSummary?> getConstructSummaryFromStateEvent() async {
try {
final Room? analyticsRoom = _client.analyticsRoomLocal(_l2!);
if (analyticsRoom == null) return null;
final state =
analyticsRoom.getState(PangeaEventTypes.constructSummary, '');
if (state == null) return null;
return ConstructSummary.fromJson(state.content);
} catch (e) {
debugPrint("Error getting construct summary room: $e");
ErrorHandler.logError(e: e, data: {'e': e});
return null;
}
}
// // extract construct use message bodies for analytics
// List<String?>? constructUseMessageContentBodies = [];
// 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 (constructUseMessageContentBodies.length !=
// constructUseOfCurrentLevel.length) {
// constructUseMessageContentBodies = null;
// }
Future<ConstructSummary?> _generateLevelUpAnalyticsAndSaveToStateEvent(
final int lowerLevel,
final int upperLevel,
) async {
// generate level up analytics as a construct summary
ConstructSummary summary;
try {
final int maxXP = constructListModel.calculateXpWithLevel(upperLevel);
final int minXP = constructListModel.calculateXpWithLevel(lowerLevel);
int diffXP = maxXP - minXP;
if (diffXP < 0) diffXP = 0;
// final request = ConstructSummaryRequest(
// constructs: constructUseOfCurrentLevel,
// constructUseMessageContentBodies: constructUseMessageContentBodies,
// language: _l2!.langCodeShort,
// upperLevel: upperLevel,
// lowerLevel: lowerLevel,
// );
// compute construct use of current level
final List<OneConstructUse> constructUseOfCurrentLevel = [];
int score = 0;
for (final use in constructListModel.uses) {
constructUseOfCurrentLevel.add(use);
score += use.xp;
if (score >= diffXP) break;
}
// final response = await ConstructRepo.generateConstructSummary(request);
// summary = response.summary;
// } catch (e) {
// debugPrint("Error generating level up analytics: $e");
// ErrorHandler.logError(e: e, data: {'e': e});
// return null;
// }
// String stateEventId;
// try {
// final Room? analyticsRoom = _client.analyticsRoomLocal(_l2!);
// if (analyticsRoom == null) {
// ErrorHandler.logError(
// e: e,
// data: {'e': e, 'message': "Analytics room not found for user"},
// );
// return null;
// }
// stateEventId = await _client.setRoomStateWithKey(
// analyticsRoom.id,
// PangeaEventTypes.constructSummary,
// '',
// summary.toJson(),
// );
// } catch (e) {
// debugPrint("Error saving construct summary room: $e");
// ErrorHandler.logError(e: e, data: {'e': e});
// return null;
// }
// return GenerateConstructSummaryResult(
// stateEventId: stateEventId,
// summary: summary,
// );
// }
// extract construct use message bodies for analytics
List<String?>? constructUseMessageContentBodies = [];
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 (constructUseMessageContentBodies.length !=
constructUseOfCurrentLevel.length) {
constructUseMessageContentBodies = null;
}
final request = ConstructSummaryRequest(
constructs: constructUseOfCurrentLevel,
constructUseMessageContentBodies: constructUseMessageContentBodies,
language: _l2!.langCodeShort,
upperLevel: upperLevel,
lowerLevel: lowerLevel,
);
final response = await ConstructRepo.generateConstructSummary(request);
summary = response.summary;
} catch (e) {
debugPrint("Error generating level up analytics: $e");
ErrorHandler.logError(e: e, data: {'e': e});
return null;
}
try {
final Room? analyticsRoom = _client.analyticsRoomLocal(_l2!);
if (analyticsRoom == null) {
ErrorHandler.logError(
data: {'message': "Analytics room not found for user"},
);
return null;
}
// don't await this, just return the original response
_saveConstructSummaryResponseToStateEvent(
summary,
);
} catch (e) {
debugPrint("Error saving construct summary room: $e");
ErrorHandler.logError(e: e, data: {'e': e});
return null;
}
return summary;
}
}
class AnalyticsCacheEntry {

View file

@ -1,7 +1,33 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:material_symbols_icons/symbols.dart';
enum LearningSkillsEnum {
writing,
reading,
speaking,
hearing,
other,
writing(isVisible: true, icon: Symbols.edit_square),
reading(isVisible: true, icon: Symbols.two_pager),
speaking(isVisible: false),
hearing(isVisible: true, icon: Icons.volume_up),
other(isVisible: false);
final bool isVisible;
final IconData icon;
const LearningSkillsEnum({
required this.isVisible,
this.icon = Icons.question_mark,
});
String tooltip(BuildContext context) {
switch (this) {
case LearningSkillsEnum.writing:
return L10n.of(context).writingExercisesTooltip;
case LearningSkillsEnum.reading:
return L10n.of(context).readingExercisesTooltip;
case LearningSkillsEnum.hearing:
return L10n.of(context).listeningExercisesTooltip;
default:
return "";
}
}
}

View file

@ -1,161 +1,425 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
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:http/http.dart' as http;
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart';
import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart';
import 'package:fluffychat/pangea/constructs/construct_repo.dart';
import 'level_summary.dart';
import 'package:fluffychat/widgets/matrix.dart';
class LevelUpConstants {
static const String starFileName = "star.png";
static const String dinoLevelUPFileName = "DinoBot-Congratulate.png";
}
class LevelUpUtil {
static void showLevelUpDialog(
static Future<void> showLevelUpDialog(
int level,
String? analyticsRoomId,
String? summaryStateEventId,
ConstructSummary? constructSummary,
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}",
),
);
showDialog(
context: context,
builder: (context) => LevelUpAnimation(
final ValueNotifier<bool> showDetailsClicked = ValueNotifier(false);
late final OverlayEntry overlayEntry;
overlayEntry = OverlayEntry(
builder: (context) => LevelUpBanner(
level: level,
analyticsRoomId: analyticsRoomId,
summaryStateEventId: summaryStateEventId,
constructSummary: constructSummary,
onDetailsClicked: () {
showDetailsClicked.value = true;
},
onOverlayExit: () {
overlayEntry.remove();
player.dispose();
},
),
).then((_) => player.dispose());
);
Overlay.of(context).insert(overlayEntry);
Future.delayed(const Duration(seconds: 5), () {
if (!showDetailsClicked.value) {
overlayEntry.remove();
player.dispose();
}
});
}
}
class LevelUpAnimation extends StatefulWidget {
class LevelUpBanner extends StatefulWidget {
final int level;
final String? analyticsRoomId;
final String? summary;
final String? summaryStateEventId;
final ConstructSummary? constructSummary;
final VoidCallback onDetailsClicked;
final VoidCallback onOverlayExit;
const LevelUpAnimation({
const LevelUpBanner({
required this.level,
required this.analyticsRoomId,
this.summary,
this.summaryStateEventId,
this.constructSummary,
required this.onDetailsClicked,
required this.onOverlayExit,
super.key,
});
@override
LevelUpAnimationState createState() => LevelUpAnimationState();
LevelUpBannerState createState() => LevelUpBannerState();
}
class LevelUpAnimationState extends State<LevelUpAnimation> {
Uint8List? bytes;
final imageURL =
"${AppConfig.assetsBaseURL}/${AnalyticsConstants.levelUpImageFileName}";
class LevelUpBannerState extends State<LevelUpBanner>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Offset> _slideAnimation;
bool _showDetails = false;
@override
void initState() {
super.initState();
_loadImageData().catchError((e) {
if (mounted) Navigator.of(context).pop();
});
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, -1),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
),
);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _loadImageData() async {
final resp =
await http.get(Uri.parse(imageURL)).timeout(const Duration(seconds: 5));
if (resp.statusCode != 200) return;
if (mounted) {
setState(() => bytes = resp.bodyBytes);
int _skillsPoints(LearningSkillsEnum skill) {
switch (skill) {
case LearningSkillsEnum.writing:
return widget.constructSummary?.writingConstructScore ?? 0;
case LearningSkillsEnum.reading:
return widget.constructSummary?.readingConstructScore ?? 0;
case LearningSkillsEnum.speaking:
return widget.constructSummary?.speakingConstructScore ?? 0;
case LearningSkillsEnum.hearing:
return widget.constructSummary?.hearingConstructScore ?? 0;
default:
return 0;
}
}
@override
Widget build(BuildContext context) {
if (bytes == null) {
return const SizedBox();
}
return Dialog(
backgroundColor: Colors.transparent,
child: Stack(
alignment: Alignment.center,
children: [
// Banner image
Image.memory(
bytes!,
height: kIsWeb ? 350 : 250,
width: double.infinity,
fit: BoxFit.cover,
return Stack(
children: [
if (_showDetails)
GestureDetector(
onTap: () {
setState(() {
_showDetails = false;
});
widget.onOverlayExit();
},
child: Container(
color: Colors.black.withAlpha(180),
),
),
// Overlay: centered title and close button
Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(
top: kIsWeb ? 200 : 100,
), // Added hardcoded padding above the text
child: Text(
L10n.of(context).levelPopupTitle(widget.level),
style: const TextStyle(
fontSize: kIsWeb ? 40 : 24,
fontWeight: FontWeight.bold,
color: Colors.white,
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,
),
),
],
),
),
],
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(L10n.of(context).close),
),
if (widget.summaryStateEventId != null &&
widget.analyticsRoomId != null)
const SizedBox(width: 16),
if (widget.summaryStateEventId != null &&
widget.analyticsRoomId != null)
// Show summary button
ElevatedButton(
onPressed: () {
showDialog(
context: context,
builder: (context) => LevelSummaryDialog(
level: widget.level,
analyticsRoomId: widget.analyticsRoomId!,
summaryStateEventId: widget.summaryStateEventId!,
constructSummary: widget.constructSummary,
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,
),
);
},
child: Text(L10n.of(context).levelSummaryTrigger),
),
],
),
],
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,
),
),
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(),
),
],
),
),
],
),
),
],
);
}
}

View file

@ -34,7 +34,11 @@ class ConstructNotificationUtil {
}
static void onClose(ConstructIdentifier construct) {
MatrixState.pAnyState.closeOverlay("${construct.string}_snackbar");
final overlayKey = "${construct.string}_snackbar";
MatrixState.pAnyState.closeOverlay(overlayKey);
MatrixState.pAnyState.activeOverlays.remove(overlayKey);
unlockedConstructs.remove(construct);
closeCompleter?.complete();
closeCompleter = null;
@ -66,8 +70,13 @@ class ConstructNotificationUtil {
canPop: false,
);
MatrixState.pAnyState.activeOverlays
.add("${construct.string}_snackbar");
await closeCompleter!.future;
} catch (e) {
MatrixState.pAnyState.activeOverlays
.remove("${construct.string}_snackbar");
showingNotification = false;
break;
}

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart';
@ -213,8 +212,8 @@ class NewConstructsBadge extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.toys_and_games,
color: ProgressIndicatorEnum.morphsUsed.color(context),
type.indicator.icon,
color: type.indicator.color(context),
size: 24,
),
const SizedBox(width: 4.0),
@ -223,7 +222,7 @@ class NewConstructsBadge extends StatelessWidget {
endValue: newConstructs,
startAnimation: opacityAnimation.value > 0.9,
style: TextStyle(
color: ProgressIndicatorEnum.morphsUsed.color(context),
color: type.indicator.color(context),
fontWeight: FontWeight.bold,
),
),

View file

@ -18,6 +18,7 @@ class OverlayListEntry {
}
class PangeaAnyState {
final Set<String> activeOverlays = {};
final Map<String, LayerLinkAndKey> _layerLinkAndKeys = {};
List<OverlayListEntry> entries = [];
@ -56,6 +57,11 @@ class PangeaAnyState {
canPop: canPop,
),
);
if (overlayKey != null) {
activeOverlays.add(overlayKey);
}
Overlay.of(context).insert(entry);
}
@ -79,6 +85,10 @@ class PangeaAnyState {
);
}
entries.remove(entry);
if (overlayKey != null) {
activeOverlays.remove(overlayKey);
}
}
}
@ -92,6 +102,7 @@ class PangeaAnyState {
.toList();
}
if (shouldRemove.isEmpty) return;
for (int i = 0; i < shouldRemove.length; i++) {
try {
shouldRemove[i].entry.remove();
@ -104,6 +115,11 @@ class PangeaAnyState {
},
);
}
if (shouldRemove[i].key != null) {
activeOverlays.remove(shouldRemove[i].key);
}
entries.remove(shouldRemove[i]);
}
}

View file

@ -13,12 +13,20 @@ class ConstructSummary {
final int lowerLevel;
final String language;
final String textSummary;
final int writingConstructScore;
final int readingConstructScore;
final int hearingConstructScore;
final int speakingConstructScore;
ConstructSummary({
required this.upperLevel,
required this.lowerLevel,
required this.language,
required this.textSummary,
required this.writingConstructScore,
required this.readingConstructScore,
required this.hearingConstructScore,
required this.speakingConstructScore,
});
Map<String, dynamic> toJson() {
@ -27,6 +35,10 @@ class ConstructSummary {
'lower_level': lowerLevel,
'language': language,
'text_summary': textSummary,
'writing_construct_score': writingConstructScore,
'reading_construct_score': readingConstructScore,
'hearing_construct_score': hearingConstructScore,
'speaking_construct_score': speakingConstructScore,
};
}
@ -36,6 +48,10 @@ class ConstructSummary {
lowerLevel: json['lower_level'],
language: json['language'],
textSummary: json['text_summary'],
writingConstructScore: json['writing_construct_score'],
readingConstructScore: json['reading_construct_score'],
hearingConstructScore: json['hearing_construct_score'],
speakingConstructScore: json['speaking_construct_score'],
);
}
}
@ -99,16 +115,6 @@ class ConstructSummaryResponse {
}
}
class GenerateConstructSummaryResult {
final String stateEventId;
final ConstructSummary summary;
GenerateConstructSummaryResult({
required this.stateEventId,
required this.summary,
});
}
class ConstructRepo {
static Future<ConstructSummaryResponse> generateConstructSummary(
ConstructSummaryRequest request,