Merge branch 'main' into 3223-marking-new-forms-and-simple-satisfying-collection-mechanic

This commit is contained in:
avashilling 2025-07-03 12:31:23 -04:00
commit 608ab95f1f
17 changed files with 366 additions and 312 deletions

View file

@ -4733,7 +4733,7 @@
"activityPlannerTitle": "Activity Planner",
"topicLabel": "Topic",
"topicPlaceholder": "Choose a topic...",
"modeLabel": "Mode",
"modeLabel": "Activity type",
"modePlaceholder": "Choose a mode...",
"learningObjectiveLabel": "Learning Objective",
"learningObjectivePlaceholder": "Choose a learning objective...",
@ -4874,7 +4874,7 @@
"exploreMore": "Explore more",
"randomize": "Randomize",
"clear": "Clear",
"makeYourOwnActivity": "Make your own activity",
"makeYourOwnActivity": "Create your own activity",
"featuredActivities": "Featured",
"yourBookmarks": "Bookmarked",
"goToChat": "Go to chat",
@ -5032,5 +5032,6 @@
}
},
"failedToFetchTranscription": "Failed to fetch transcription",
"deleteEmptySpaceDesc": "The space will be deleted for all participants. This action cannot be undone."
"deleteEmptySpaceDesc": "The space will be deleted for all participants. This action cannot be undone.",
"regenerate": "Regenerate"
}

View file

@ -5362,7 +5362,6 @@
"activityPlannerTitle": "Planificador de Actividades",
"topicLabel": "Tema",
"topicPlaceholder": "Elige un tema...",
"modeLabel": "Modo",
"modePlaceholder": "Elige un modo...",
"learningObjectiveLabel": "Objetivo de Aprendizaje",
"learningObjectivePlaceholder": "Elige un objetivo de aprendizaje...",
@ -5494,7 +5493,6 @@
"exploreMore": "Explorar más",
"randomize": "Aleatorizar",
"clear": "Limpiar",
"makeYourOwnActivity": "Crea tu propia actividad",
"featuredActivities": "Destacadas",
"yourBookmarks": "Marcados",
"goToChat": "Ir al chat",

View file

@ -3551,7 +3551,6 @@
"activityPlannerTitle": "Trình lập hoạt động",
"topicLabel": "Chủ đề",
"topicPlaceholder": "Chọn một chủ đề...",
"modeLabel": "Chế độ",
"modePlaceholder": "Chọn một chế độ...",
"learningObjectiveLabel": "Mục tiêu học tập",
"learningObjectivePlaceholder": "Chọn một mục tiêu học tập...",
@ -3834,7 +3833,6 @@
"exploreMore": "Khám phá thêm",
"randomize": "Ngẫu nhiên hóa",
"clear": "Xóa",
"makeYourOwnActivity": "Tạo hoạt động của riêng bạn",
"featuredActivities": "Nổi bật",
"yourBookmarks": "Đã đánh dấu",
"goToChat": "Đi đến trò chuyện",

View file

@ -16,6 +16,7 @@ import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:just_audio/just_audio.dart';
import 'package:matrix/matrix.dart';
import 'package:path_provider/path_provider.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -506,12 +507,21 @@ class ChatController extends State<ChatPageWithRoom>
);
if (audioFile == null) return;
matrix.audioPlayer!.setAudioSource(
BytesAudioSource(
audioFile.bytes,
audioFile.mimeType,
),
);
if (!kIsWeb) {
final tempDir = await getTemporaryDirectory();
File? file;
file = File('${tempDir.path}/${audioFile.name}');
await file.writeAsBytes(audioFile.bytes);
matrix.audioPlayer!.setFilePath(file.path);
} else {
matrix.audioPlayer!.setAudioSource(
BytesAudioSource(
audioFile.bytes,
audioFile.mimeType,
),
);
}
matrix.audioPlayer!.play();
});

View file

@ -143,15 +143,28 @@ class _MessageSearchResultListTile extends StatelessWidget {
size: 16,
),
const SizedBox(width: 8),
Text(
displayname,
),
Expanded(
// #Pangea
// Text(
// displayname,
// ),
// Expanded(
// child: Text(
// ' | ${event.originServerTs.localizedTimeShort(context)}',
// style: const TextStyle(fontSize: 12),
// ),
// ),
Flexible(
child: Text(
' | ${event.originServerTs.localizedTimeShort(context)}',
style: const TextStyle(fontSize: 12),
displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Text(
' | ${event.originServerTs.localizedTimeShort(context)}',
style: const TextStyle(fontSize: 12),
),
// Pangea#
],
),
subtitle: Linkify(

View file

@ -10,7 +10,6 @@ import 'package:fluffychat/pangea/activity_planner/activity_mode_list_repo.dart'
import 'package:fluffychat/pangea/activity_planner/activity_plan_generation_repo.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_response.dart';
import 'package:fluffychat/pangea/activity_planner/learning_objective_list_repo.dart';
import 'package:fluffychat/pangea/activity_planner/list_request_schema.dart';
import 'package:fluffychat/pangea/activity_planner/media_enum.dart';
@ -166,11 +165,6 @@ class ActivityGeneratorState extends State<ActivityGenerator> {
setState(() => selectedCefrLevel = value);
}
void setSelectedMedia(MediaEnum? value) {
if (value == null) return;
setState(() => selectedMedia = value);
}
Future<ActivitySettingResponseSchema?> get _selectedMode async {
final modes = await modeItems;
return modes.firstWhereOrNull(
@ -203,30 +197,18 @@ class ActivityGeneratorState extends State<ActivityGenerator> {
});
}
Future<void> onEdit(int index, ActivityPlanModel updatedActivity) async {
// in this case we're editing an activity plan that was generated recently
// via the repo and should be updated in the cached response
if (activities != null) {
activities?[index] = updatedActivity;
ActivityPlanGenerationRepo.set(
planRequest,
ActivityPlanResponse(activityPlans: activities!),
);
}
setState(() {});
}
void update() => setState(() {});
Future<void> generate() async {
Future<void> generate({bool force = false}) async {
setState(() {
loading = true;
error = null;
activities = null;
});
try {
final resp = await ActivityPlanGenerationRepo.get(planRequest);
final resp = await ActivityPlanGenerationRepo.get(
planRequest,
force: force,
);
activities = resp.activityPlans;
await _setModeImageURL();
} catch (e, s) {

View file

@ -61,6 +61,7 @@ class ActivityGeneratorView extends StatelessWidget {
room: controller.room,
builder: (c) {
return ActivityPlanCard(
regenerate: () => controller.generate(force: true),
controller: c,
);
},

View file

@ -20,10 +20,12 @@ import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_en
import 'package:fluffychat/widgets/future_loading_dialog.dart';
class ActivityPlanCard extends StatefulWidget {
final VoidCallback regenerate;
final ActivityPlannerBuilderState controller;
const ActivityPlanCard({
super.key,
required this.regenerate,
required this.controller,
});
@ -121,8 +123,11 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
AnimatedSize(
duration: FluffyThemes.animationDuration,
child: Stack(
alignment: Alignment.bottomCenter,
children: [
Container(
width: 200.0,
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
),
@ -131,6 +136,7 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
child: widget.controller.imageURL != null ||
widget.controller.avatar != null
? ClipRRect(
borderRadius: BorderRadius.circular(20.0),
child: widget.controller.avatar == null
? CachedNetworkImage(
fit: BoxFit.cover,
@ -156,14 +162,17 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
),
),
if (widget.controller.isEditing)
Positioned(
top: 10.0,
right: 10.0,
child: IconButton(
icon: const Icon(Icons.upload_outlined),
onPressed: widget.controller.selectAvatar,
style: IconButton.styleFrom(
backgroundColor: Colors.black,
InkWell(
borderRadius: BorderRadius.circular(90),
onTap: widget.controller.selectAvatar,
child: CircleAvatar(
backgroundColor:
Theme.of(context).colorScheme.secondary,
radius: 16.0,
child: Icon(
Icons.add_a_photo_outlined,
size: 16.0,
color: Theme.of(context).colorScheme.onSecondary,
),
),
),
@ -368,47 +377,108 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
),
],
const SizedBox(height: itemPadding),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Tooltip(
message: !widget.controller.isEditing
? l10n.edit
: l10n.saveChanges,
child: IconButton(
icon: Icon(
!widget.controller.isEditing
? Icons.edit
: Icons.save,
widget.controller.isEditing
? Row(
spacing: 12.0,
children: [
Expanded(
child: ElevatedButton(
onPressed: widget.controller.saveEdits,
child: Row(
children: [
const Icon(Icons.save),
Expanded(
child: Text(
L10n.of(context).save,
textAlign: TextAlign.center,
),
),
],
),
),
onPressed: () => !widget.controller.isEditing
? setState(() {
widget.controller.isEditing = true;
})
: widget.controller.saveEdits(),
isSelected: widget.controller.isEditing,
),
),
if (widget.controller.isEditing)
Tooltip(
message: l10n.cancel,
child: IconButton(
icon: const Icon(Icons.cancel),
Expanded(
child: ElevatedButton(
onPressed: widget.controller.clearEdits,
child: Row(
children: [
const Icon(Icons.cancel),
Expanded(
child: Text(
L10n.of(context).cancel,
textAlign: TextAlign.center,
),
),
],
),
),
),
],
),
ElevatedButton.icon(
onPressed:
!widget.controller.isEditing ? _onLaunch : null,
icon: const Icon(Icons.send),
label: Text(l10n.launchActivityButton),
),
],
),
],
)
: Column(
spacing: 12.0,
children: [
Row(
spacing: 12.0,
children: [
Expanded(
child: ElevatedButton(
child: Row(
children: [
const Icon(Icons.edit),
Expanded(
child: Text(
L10n.of(context).edit,
textAlign: TextAlign.center,
),
),
],
),
onPressed: () =>
widget.controller.setEditing(true),
),
),
Expanded(
child: ElevatedButton(
onPressed: widget.regenerate,
child: Row(
children: [
const Icon(Icons.lightbulb_outline),
Expanded(
child: Text(
L10n.of(context).regenerate,
textAlign: TextAlign.center,
),
),
],
),
),
),
],
),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: _onLaunch,
child: Row(
children: [
const Icon(Icons.send),
Expanded(
child: Text(
L10n.of(context)
.launchActivityButton,
textAlign: TextAlign.center,
),
),
],
),
),
),
],
),
],
),
],
),
),

View file

@ -18,9 +18,12 @@ class ActivityPlanGenerationRepo {
_activityPlanStorage.write(request.storageKey, response.toJson());
}
static Future<ActivityPlanResponse> get(ActivityPlanRequest request) async {
static Future<ActivityPlanResponse> get(
ActivityPlanRequest request, {
bool force = false,
}) async {
final cachedJson = _activityPlanStorage.read(request.storageKey);
if (cachedJson != null) {
if (cachedJson != null && !force) {
final cached = ActivityPlanResponse.fromJson(cachedJson);
return cached;

View file

@ -7,6 +7,7 @@ import 'package:http/http.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
@ -21,18 +22,12 @@ class ActivityPlannerBuilder extends StatefulWidget {
final Widget Function(ActivityPlannerBuilderState) builder;
final Future<void> Function(
String,
ActivityPlanModel,
)? onEdit;
const ActivityPlannerBuilder({
super.key,
required this.initialActivity,
this.initialFilename,
this.room,
required this.builder,
this.onEdit,
});
@override
@ -206,12 +201,10 @@ class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
if (!formKey.currentState!.validate()) return;
await updateImageURL();
setEditing(false);
if (widget.onEdit != null) {
await widget.onEdit!(
widget.initialActivity.bookmarkId,
updatedActivity,
);
}
await BookmarkedActivitiesRepo.remove(widget.initialActivity.bookmarkId);
await BookmarkedActivitiesRepo.save(updatedActivity);
if (mounted) setState(() {});
}
Future<void> clearEdits() async {

View file

@ -36,15 +36,6 @@ class BookmarkedActivitiesListState extends State<BookmarkedActivitiesList> {
double get cardHeight => _isColumnMode ? 325.0 : 250.0;
double get cardWidth => _isColumnMode ? 225.0 : 150.0;
Future<void> _onEdit(
String activityId,
ActivityPlanModel activity,
) async {
await BookmarkedActivitiesRepo.remove(activityId);
await BookmarkedActivitiesRepo.save(activity);
if (mounted) setState(() {});
}
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context);
@ -77,7 +68,6 @@ class BookmarkedActivitiesListState extends State<BookmarkedActivitiesList> {
builder: (context) {
return ActivityPlannerBuilder(
initialActivity: activity,
onEdit: _onEdit,
room: widget.room,
builder: (controller) {
return ActivitySuggestionDialog(

View file

@ -474,100 +474,87 @@ class GetAnalyticsController extends BaseController {
}
}
Future<ConstructSummary?> generateLevelUpAnalytics(
Future<ConstructSummary> generateLevelUpAnalytics(
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 int maxXP = constructListModel.calculateXpWithLevel(upperLevel);
final int minXP = constructListModel.calculateXpWithLevel(lowerLevel);
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.xp;
if (score >= diffXP) break;
}
// 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;
}
// extract construct use message bodies for analytics
final Map<String, Set<String>> useEventIds = {};
for (final use in constructUseOfCurrentLevel) {
if (use.metadata.roomId == null) continue;
if (use.metadata.eventId == null) continue;
useEventIds[use.metadata.roomId!] ??= {};
useEventIds[use.metadata.roomId!]!.add(use.metadata.eventId!);
}
// extract construct use message bodies for analytics
final Map<String, Set<String>> useEventIds = {};
for (final use in constructUseOfCurrentLevel) {
if (use.metadata.roomId == null) continue;
if (use.metadata.eventId == null) continue;
useEventIds[use.metadata.roomId!] ??= {};
useEventIds[use.metadata.roomId!]!.add(use.metadata.eventId!);
}
final List<String?> constructUseMessageContentBodies = [];
for (final entry in useEventIds.entries) {
final String roomId = entry.key;
final room = _client.getRoomById(roomId);
if (room == null) continue;
final List<String?> messageBodies = [];
for (final eventId in entry.value) {
try {
final Event? event = await room.getEventById(eventId);
if (event?.content["body"] is! String) continue;
final String body = event?.content["body"] as String;
if (body.isEmpty) continue;
messageBodies.add(body);
} catch (e, s) {
debugPrint("Error getting event by ID: $e");
ErrorHandler.logError(
e: e,
s: s,
data: {
'roomId': roomId,
'eventId': eventId,
},
);
continue;
}
final List<String?> constructUseMessageContentBodies = [];
for (final entry in useEventIds.entries) {
final String roomId = entry.key;
final room = _client.getRoomById(roomId);
if (room == null) continue;
final List<String?> messageBodies = [];
for (final eventId in entry.value) {
try {
final Event? event = await room.getEventById(eventId);
if (event?.content["body"] is! String) continue;
final String body = event?.content["body"] as String;
if (body.isEmpty) continue;
messageBodies.add(body);
} catch (e, s) {
debugPrint("Error getting event by ID: $e");
ErrorHandler.logError(
e: e,
s: s,
data: {
'roomId': roomId,
'eventId': eventId,
},
);
continue;
}
constructUseMessageContentBodies.addAll(messageBodies);
}
final request = ConstructSummaryRequest(
constructs: constructUseOfCurrentLevel,
constructUseMessageContentBodies: constructUseMessageContentBodies,
language: _l1!.langCodeShort,
upperLevel: upperLevel,
lowerLevel: lowerLevel,
);
final response = await ConstructRepo.generateConstructSummary(request);
summary = response.summary;
summary.levelVocabConstructs = MatrixState
.pangeaController.getAnalytics.constructListModel.vocabLemmas;
summary.levelGrammarConstructs = MatrixState
.pangeaController.getAnalytics.constructListModel.grammarLemmas;
} catch (e) {
debugPrint("Error generating level up analytics: $e");
ErrorHandler.logError(e: e, data: {'e': e});
return null;
constructUseMessageContentBodies.addAll(messageBodies);
}
try {
final Room? analyticsRoom = await _client.getMyAnalyticsRoom(_l2!);
if (analyticsRoom == null) {
throw "Analytics room not found for user";
}
final request = ConstructSummaryRequest(
constructs: constructUseOfCurrentLevel,
constructUseMessageContentBodies: constructUseMessageContentBodies,
language: _l1!.langCodeShort,
upperLevel: upperLevel,
lowerLevel: lowerLevel,
);
// don't await this, just return the original response
_saveConstructSummaryResponseToStateEvent(
summary,
);
} catch (e, s) {
debugPrint("Error saving construct summary room: $e");
ErrorHandler.logError(e: e, s: s, data: {'e': e});
final response = await ConstructRepo.generateConstructSummary(request);
final ConstructSummary summary = response.summary;
summary.levelVocabConstructs = MatrixState
.pangeaController.getAnalytics.constructListModel.vocabLemmas;
summary.levelGrammarConstructs = MatrixState
.pangeaController.getAnalytics.constructListModel.grammarLemmas;
final Room? analyticsRoom = await _client.getMyAnalyticsRoom(_l2!);
if (analyticsRoom == null) {
throw "Analytics room not found for user";
}
// don't await this, just return the original response
_saveConstructSummaryResponseToStateEvent(
summary,
);
return summary;
}
}

View file

@ -12,6 +12,7 @@ import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_manager.dart'
import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_popup.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/utils/overlay.dart';
import 'package:fluffychat/pangea/constructs/construct_repo.dart';
import 'package:fluffychat/widgets/matrix.dart';
class LevelUpConstants {
@ -95,10 +96,15 @@ class LevelUpBannerState extends State<LevelUpBanner>
bool _showedDetails = false;
final Completer<ConstructSummary> _constructSummaryCompleter =
Completer<ConstructSummary>();
@override
void initState() {
super.initState();
_loadConstructSummary();
LevelUpManager.instance.preloadAnalytics(
context,
widget.level,
@ -149,10 +155,23 @@ class LevelUpBannerState extends State<LevelUpBanner>
await showDialog(
context: context,
builder: (context) => const LevelUpPopup(),
builder: (context) => LevelUpPopup(
constructSummaryCompleter: _constructSummaryCompleter,
),
);
}
Future<void> _loadConstructSummary() async {
try {
final summary = MatrixState.pangeaController.getAnalytics
.generateLevelUpAnalytics(widget.prevLevel, widget.level);
_constructSummaryCompleter.complete(summary);
} catch (e) {
debugPrint("Error generating level up analytics: $e");
_constructSummaryCompleter.completeError(e);
}
}
@override
Widget build(BuildContext context) {
final isColumnMode = FluffyThemes.isColumnMode(context);

View file

@ -22,13 +22,8 @@ class LevelUpManager {
int prevVocab = 0;
int nextVocab = 0;
String? userL2Code;
ConstructSummary? constructSummary;
bool hasSeenPopup = false;
bool shouldAutoPopup = false;
String? error;
Future<void> preloadAnalytics(
BuildContext context,
@ -46,12 +41,6 @@ class LevelUpManager {
nextVocab = MatrixState
.pangeaController.getAnalytics.constructListModel.vocabLemmas;
userL2Code = MatrixState.pangeaController.languageController
.activeL2Code()
?.toUpperCase();
getConstructFromLevelUp();
final LanguageModel? l2 =
MatrixState.pangeaController.languageController.userL2;
final Room? analyticsRoom =
@ -91,28 +80,6 @@ class LevelUpManager {
}
}
//for testing, just fetch last level up from saved analytics
void getConstructFromButton() {
constructSummary = MatrixState.pangeaController.getAnalytics
.getConstructSummaryFromStateEvent();
debugPrint(
"Last saved construct summary from analytics controller function: ${constructSummary?.toJson()}",
);
}
//for getting real level up data when leveled up
void getConstructFromLevelUp() async {
try {
constructSummary = await MatrixState.pangeaController.getAnalytics
.generateLevelUpAnalytics(
prevLevel,
level,
);
} catch (e) {
error = e.toString();
}
}
void markPopupSeen() {
hasSeenPopup = true;
shouldAutoPopup = false;
@ -127,7 +94,5 @@ class LevelUpManager {
nextGrammar = 0;
prevVocab = 0;
nextVocab = 0;
constructSummary = null;
error = null;
}
}

View file

@ -20,12 +20,15 @@ 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';
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
class LevelUpPopup extends StatelessWidget {
final Completer<ConstructSummary> constructSummaryCompleter;
const LevelUpPopup({
required this.constructSummaryCompleter,
super.key,
});
@ -50,6 +53,7 @@ class LevelUpPopup extends StatelessWidget {
body: LevelUpPopupContent(
prevLevel: LevelUpManager.instance.prevLevel,
level: LevelUpManager.instance.level,
constructSummaryCompleter: constructSummaryCompleter,
),
),
);
@ -59,11 +63,13 @@ class LevelUpPopup extends StatelessWidget {
class LevelUpPopupContent extends StatefulWidget {
final int prevLevel;
final int level;
final Completer<ConstructSummary> constructSummaryCompleter;
const LevelUpPopupContent({
super.key,
required this.prevLevel,
required this.level,
required this.constructSummaryCompleter,
});
@override
@ -72,55 +78,39 @@ class LevelUpPopupContent extends StatefulWidget {
class _LevelUpPopupContentState extends State<LevelUpPopupContent>
with SingleTickerProviderStateMixin {
late int _endGrammar;
late int _endVocab;
final int _startGrammar = LevelUpManager.instance.prevGrammar;
final int _startVocab = LevelUpManager.instance.prevVocab;
Timer? _summaryPollTimer;
final String? _error = LevelUpManager.instance.error;
String language = LevelUpManager.instance.userL2Code ?? "N/A";
late final AnimationController _controller;
late final ConfettiController _confettiController;
bool _hasBlastedConfetti = false;
final Duration _animationDuration = const Duration(seconds: 5);
Uri? avatarUrl;
late final Future<Profile> profile;
int displayedLevel = -1;
late ConstructSummary? _constructSummary;
Uri? avatarUrl;
bool _hasBlastedConfetti = false;
String language = MatrixState.pangeaController.languageController
.activeL2Code()
?.toUpperCase() ??
LanguageKeys.unknownLanguage;
ConstructSummary? _constructSummary;
Object? _error;
bool _loading = true;
@override
void initState() {
super.initState();
_loadConstructSummary();
LevelUpManager.instance.markPopupSeen();
displayedLevel = widget.prevLevel;
_confettiController =
ConfettiController(duration: const Duration(seconds: 1));
_endGrammar = LevelUpManager.instance.nextGrammar;
_endVocab = LevelUpManager.instance.nextVocab;
_constructSummary = LevelUpManager.instance.constructSummary;
// Poll for constructSummary if not available
if (_constructSummary == null) {
_summaryPollTimer =
Timer.periodic(const Duration(milliseconds: 300), (timer) {
final summary = LevelUpManager.instance.constructSummary;
if (summary != null) {
setState(() {
_constructSummary = summary;
});
timer.cancel();
}
});
}
final client = Matrix.of(context).client;
client.fetchOwnProfile().then((profile) {
setState(() {
avatarUrl = profile.avatarUrl;
});
setState(() => avatarUrl = profile.avatarUrl);
});
_controller = AnimationController(
duration: _animationDuration,
duration: const Duration(seconds: 5),
vsync: this,
);
@ -135,7 +125,6 @@ class _LevelUpPopupContentState extends State<LevelUpPopupContent>
_controller.addListener(() {
if (_controller.value >= 0.5 && !_hasBlastedConfetti) {
//_confettiController.play();
_hasBlastedConfetti = true;
rainConfetti(context);
}
@ -146,7 +135,6 @@ class _LevelUpPopupContentState extends State<LevelUpPopupContent>
@override
void dispose() {
_summaryPollTimer?.cancel();
_controller.dispose();
_confettiController.dispose();
LevelUpManager.instance.reset();
@ -154,6 +142,22 @@ class _LevelUpPopupContentState extends State<LevelUpPopupContent>
super.dispose();
}
int get _startGrammar => LevelUpManager.instance.prevGrammar;
int get _startVocab => LevelUpManager.instance.prevVocab;
get _endGrammar => LevelUpManager.instance.nextGrammar;
get _endVocab => LevelUpManager.instance.nextVocab;
Future<void> _loadConstructSummary() async {
try {
_constructSummary = await widget.constructSummaryCompleter.future;
} catch (e) {
_error = e;
} finally {
setState(() => _loading = false);
}
}
int _getSkillXP(LearningSkillsEnum skill) {
if (_constructSummary == null) return 0;
return switch (skill) {
@ -368,52 +372,60 @@ class _LevelUpPopupContentState extends State<LevelUpPopupContent>
),
),
const SizedBox(height: 16),
// Skills section
AnimatedBuilder(
animation: skillsOpacity,
builder: (_, __) => Opacity(
opacity: skillsOpacity.value,
child: _error == null
? Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildSkillsTable(context),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text(
_constructSummary?.textSummary ??
L10n.of(context).loadingPleaseWait,
textAlign: TextAlign.left,
style: Theme.of(context).textTheme.bodyMedium,
),
),
Padding(
padding: const EdgeInsets.all(24.0),
child: CachedNetworkImage(
imageUrl:
"${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}",
width: 400,
fit: BoxFit.cover,
),
),
],
)
// if error getting construct summary
: Row(
children: [
Tooltip(
message: L10n.of(context).oopsSomethingWentWrong,
child: Icon(
Icons.error,
color: Theme.of(context).colorScheme.error,
),
),
],
if (_loading)
const Center(
child: SizedBox(
height: 50,
width: 50,
child: CircularProgressIndicator(
strokeWidth: 2.0,
color: AppConfig.goldLight,
),
),
)
else if (_error != null)
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
L10n.of(context).oopsSomethingWentWrong,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 16,
),
),
)
else if (_constructSummary != null)
// Skills section
AnimatedBuilder(
animation: skillsOpacity,
builder: (_, __) => Opacity(
opacity: skillsOpacity.value,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildSkillsTable(context),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text(
_constructSummary!.textSummary,
textAlign: TextAlign.left,
style: Theme.of(context).textTheme.bodyMedium,
),
),
Padding(
padding: const EdgeInsets.all(24.0),
child: CachedNetworkImage(
imageUrl:
"${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}",
width: 400,
fit: BoxFit.cover,
),
),
],
),
),
),
),
// Share button, currently no functionality
// ElevatedButton(
// onPressed: () {

View file

@ -12,9 +12,15 @@ ConfettiController? _rainController;
void rainConfetti(BuildContext context) {
if (_confettiEntry != null) return; // Prevent duplicates
int numParticles = 2;
_blastController = ConfettiController(duration: const Duration(seconds: 1));
_rainController = ConfettiController(duration: const Duration(seconds: 3));
_rainController = ConfettiController(duration: const Duration(seconds: 8));
Future.delayed(const Duration(seconds: 4), () {
if (_rainController?.state == ConfettiControllerState.playing) {
numParticles = 1;
}
});
_blastController!.play();
_rainController!.play();
@ -61,14 +67,14 @@ void rainConfetti(BuildContext context) {
confettiController: _rainController!,
blastDirectionality: BlastDirectionality.directional,
blastDirection: 3 * pi / 2,
shouldLoop: true,
shouldLoop: false,
maxBlastForce: 5,
minBlastForce: 2,
minimumSize: const Size(20, 20),
maximumSize: const Size(25, 25),
gravity: 0.07,
emissionFrequency: 0.1,
numberOfParticles: 2,
numberOfParticles: numParticles,
colors: const [AppConfig.goldLight, AppConfig.gold],
createParticlePath: drawStar,
),

View file

@ -158,7 +158,10 @@ class OverlayMessage extends StatelessWidget {
FluffyThemes.columnWidth * 1.5,
MediaQuery.of(context).size.width -
(ownMessage ? 0 : Avatar.defaultSize) -
24.0,
32.0 -
(FluffyThemes.isColumnMode(context)
? FluffyThemes.columnWidth + FluffyThemes.navRailWidth
: 0.0),
),
),
child: Padding(
@ -243,7 +246,10 @@ class OverlayMessage extends StatelessWidget {
FluffyThemes.columnWidth * 1.5,
MediaQuery.of(context).size.width -
(ownMessage ? 0 : Avatar.defaultSize) -
24.0,
32.0 -
(FluffyThemes.isColumnMode(context)
? FluffyThemes.columnWidth + FluffyThemes.navRailWidth
: 0.0),
),
),
child: Padding(