Merge branch 'main' into 3223-marking-new-forms-and-simple-satisfying-collection-mechanic
This commit is contained in:
commit
608ab95f1f
17 changed files with 366 additions and 312 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ class ActivityGeneratorView extends StatelessWidget {
|
|||
room: controller.room,
|
||||
builder: (c) {
|
||||
return ActivityPlanCard(
|
||||
regenerate: () => controller.generate(force: true),
|
||||
controller: c,
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: () {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue