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:
parent
4b56b8adb7
commit
1e20d5fb2c
10 changed files with 562 additions and 219 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue