feat: activity dropdown menu
This commit is contained in:
parent
5ce2a787b4
commit
b45541d826
11 changed files with 593 additions and 224 deletions
|
|
@ -100,8 +100,12 @@ abstract class FluffyThemes {
|
|||
toolbarHeight: isColumnMode ? 72 : 56,
|
||||
shadowColor:
|
||||
isColumnMode ? colorScheme.surfaceContainer.withAlpha(128) : null,
|
||||
surfaceTintColor: isColumnMode ? colorScheme.surface : null,
|
||||
backgroundColor: isColumnMode ? colorScheme.surface : null,
|
||||
// #Pangea
|
||||
// surfaceTintColor: isColumnMode ? colorScheme.surface : null,
|
||||
// backgroundColor: isColumnMode ? colorScheme.surface : null,
|
||||
surfaceTintColor: colorScheme.surface,
|
||||
backgroundColor: colorScheme.surface,
|
||||
// Pangea#
|
||||
systemOverlayStyle: SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: brightness.reversed,
|
||||
|
|
|
|||
|
|
@ -5197,6 +5197,31 @@
|
|||
"type": "String",
|
||||
"course": {}
|
||||
},
|
||||
"activityComplete": "This activity has been completed. The activity summary should be available below.",
|
||||
"haventChattedMuch": "It looks like you haven't chatted much, try using some more vocab words! If you feel like you've completed your objective, you can end the activity below.",
|
||||
"haveChatted": "It looks like you've been chatting for a while! If you feel like you've completed your objective, wrap up to finish the activity and we'll generate you a summary in the chat!",
|
||||
"userDoneAndWaiting": "{num1}/{num2} participants have wrapped up. Wait for everyone to finish, and we'll generate you a summary in the chat! \n\nIf you'd like to rejoin the conversation, click the continue button in the chat.",
|
||||
"@userDoneAndWaiting": {
|
||||
"placeholders": {
|
||||
"num1": {
|
||||
"type": "int"
|
||||
},
|
||||
"num2": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"othersDoneAndWaiting": "${num1}/{num2} are done. Have you completed your objective?",
|
||||
"@othersDoneAndWaiting": {
|
||||
"placeholders": {
|
||||
"num1": {
|
||||
"type": "int"
|
||||
},
|
||||
"num2": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"startNewSession": "Start new session",
|
||||
"joinOpenSession": "Join open session",
|
||||
"less": "less",
|
||||
|
|
|
|||
|
|
@ -2218,6 +2218,11 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
void toggleShowInstructions() {
|
||||
if (mounted) setState(() => showInstructions = !showInstructions);
|
||||
}
|
||||
|
||||
bool showActivityDropdown = false;
|
||||
void setShowDropdown(bool show) async {
|
||||
setState(() => showActivityDropdown = show);
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
late final ValueNotifier<bool> _displayChatDetailsColumn;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_stats_button.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/sync_status_localization.dart';
|
||||
|
|
@ -28,6 +30,23 @@ class ChatAppBarTitle extends StatelessWidget {
|
|||
// ),
|
||||
// );
|
||||
// }
|
||||
if (controller.room.showActivityChatUI) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
controller.room.getLocalizedDisplayname(),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
ActivityStatsButton(controller: controller),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
// Pangea#
|
||||
return InkWell(
|
||||
hoverColor: Colors.transparent,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import 'package:fluffychat/pages/chat/chat_event_list.dart';
|
|||
import 'package:fluffychat/pages/chat/pinned_events.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_finished_status_message.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_pinned_message.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_stats_menu.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/load_activity_summary_widget.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/chat_input_bar.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/chat_input_bar_header.dart';
|
||||
|
|
@ -216,6 +216,9 @@ class ChatView extends StatelessWidget {
|
|||
// backgroundColor: controller.selectedEvents.isEmpty
|
||||
// ? null
|
||||
// : theme.colorScheme.tertiaryContainer,
|
||||
toolbarHeight:
|
||||
controller.room.showActivityChatUI ? 106.0 : null,
|
||||
centerTitle: controller.room.showActivityChatUI,
|
||||
// Pangea#
|
||||
automaticallyImplyLeading: false,
|
||||
leading: controller.selectMode
|
||||
|
|
@ -254,6 +257,13 @@ class ChatView extends StatelessWidget {
|
|||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// #Pangea
|
||||
if (!controller.showActivityDropdown)
|
||||
Divider(
|
||||
height: 1,
|
||||
color: theme.dividerColor,
|
||||
),
|
||||
// Pangea#
|
||||
PinnedEvents(controller),
|
||||
if (scrollUpBannerEventId != null)
|
||||
ChatAppBarListTile(
|
||||
|
|
@ -480,7 +490,7 @@ class ChatView extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
ActivityPinnedMessage(controller),
|
||||
ActivityStatsMenu(controller),
|
||||
// Pangea#
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -118,7 +118,14 @@ class ActivityPlanModel {
|
|||
/// use target emoji for learning objective
|
||||
/// use step emoji for instructions
|
||||
String get markdown {
|
||||
String markdown = '''🎯 $learningObjective \n🪜 $instructions \n\n📖''';
|
||||
final String markdown =
|
||||
'''🎯 $learningObjective \n🪜 $instructions \n\n📖 $vocabString''';
|
||||
return markdown;
|
||||
}
|
||||
|
||||
String get vocabString {
|
||||
final List<String> vocabList = [];
|
||||
String vocabString = "";
|
||||
// cycle through vocab with index
|
||||
for (var i = 0; i < vocab.length; i++) {
|
||||
// if the lemma appears more than once in the vocab list, show the pos
|
||||
|
|
@ -126,10 +133,25 @@ class ActivityPlanModel {
|
|||
final v = vocab[i];
|
||||
final bool showPos =
|
||||
vocab.where((vocab) => vocab.lemma == v.lemma).length > 1;
|
||||
markdown +=
|
||||
vocabString +=
|
||||
'${v.lemma}${showPos ? ' (${v.pos})' : ''}${i + 1 < vocab.length ? ', ' : ''}';
|
||||
vocabList.add("${v.lemma}${showPos ? ' (${v.pos})' : ''}");
|
||||
}
|
||||
return markdown;
|
||||
return vocabString;
|
||||
}
|
||||
|
||||
List get vocabList {
|
||||
final List<String> vocabList = [];
|
||||
// cycle through vocab with index
|
||||
for (var i = 0; i < vocab.length; i++) {
|
||||
// if the lemma appears more than once in the vocab list, show the pos
|
||||
// vocab is a wrapped list of string, separated by commas
|
||||
final v = vocab[i];
|
||||
final bool showPos =
|
||||
vocab.where((vocab) => vocab.lemma == v.lemma).length > 1;
|
||||
vocabList.add("${v.lemma}${showPos ? ' (${v.pos})' : ''}");
|
||||
}
|
||||
return vocabList;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -1,216 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_session_constants.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
|
||||
class ActivityPinnedMessage extends StatefulWidget {
|
||||
final ChatController controller;
|
||||
const ActivityPinnedMessage(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
State<ActivityPinnedMessage> createState() => ActivityPinnedMessageState();
|
||||
}
|
||||
|
||||
class ActivityPinnedMessageState extends State<ActivityPinnedMessage> {
|
||||
bool _showDropdown = false;
|
||||
|
||||
Room get room => widget.controller.room;
|
||||
|
||||
void _scrollToActivity() {
|
||||
final eventId = widget.controller.timeline?.events
|
||||
.firstWhereOrNull(
|
||||
(e) => e.type == PangeaEventTypes.activityPlan,
|
||||
)
|
||||
?.eventId;
|
||||
if (eventId == null) return;
|
||||
widget.controller.scrollToEventId(eventId);
|
||||
}
|
||||
|
||||
void _setShowDropdown(bool value) {
|
||||
if (value != _showDropdown) {
|
||||
setState(() {
|
||||
_showDropdown = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _finishActivity({bool forAll = false}) async {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
forAll
|
||||
? await room.finishActivityForAll()
|
||||
: await room.finishActivity();
|
||||
if (mounted) {
|
||||
_setShowDropdown(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// if the room has no activity, or if it doesn't have the permission
|
||||
// levels for sending the required events, don't show the pinned message
|
||||
if (!room.isActiveInActivity) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final theme = Theme.of(context);
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
|
||||
return Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: _showDropdown ? 0 : null,
|
||||
child: Column(
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
decoration: BoxDecoration(
|
||||
color: _showDropdown
|
||||
? theme.colorScheme.surfaceContainerHighest
|
||||
: theme.colorScheme.surface,
|
||||
),
|
||||
child: ChatAppBarListTile(
|
||||
title: "🎯 ${room.activityPlan!.learningObjective}",
|
||||
leading: const SizedBox(width: 18.0),
|
||||
trailing: Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: ElevatedButton(
|
||||
onPressed:
|
||||
_showDropdown ? null : () => _setShowDropdown(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: Size.zero,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
backgroundColor: AppConfig.yellowDark,
|
||||
foregroundColor: theme.colorScheme.surface,
|
||||
disabledBackgroundColor:
|
||||
AppConfig.yellowDark.withAlpha(100),
|
||||
disabledForegroundColor:
|
||||
theme.colorScheme.surface.withAlpha(100),
|
||||
),
|
||||
child: Text(
|
||||
L10n.of(context).endActivityTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: _scrollToActivity,
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: Curves.easeInOut,
|
||||
child: ClipRect(
|
||||
child: _showDropdown
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 16.0,
|
||||
),
|
||||
child: Column(
|
||||
spacing: 12.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).endActivityDesc,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: isColumnMode ? 16.0 : 12.0,
|
||||
),
|
||||
),
|
||||
CachedNetworkImage(
|
||||
imageUrl:
|
||||
"${AppConfig.assetsBaseURL}/${ActivitySessionConstants.endActivityAssetPath}",
|
||||
width: isColumnMode ? 240.0 : 120.0,
|
||||
),
|
||||
Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
foregroundColor:
|
||||
theme.colorScheme.onSecondary,
|
||||
backgroundColor:
|
||||
theme.colorScheme.secondary,
|
||||
),
|
||||
onPressed: _finishActivity,
|
||||
child: Text(
|
||||
L10n.of(context).endActivityTitle,
|
||||
style: TextStyle(
|
||||
fontSize: isColumnMode ? 16.0 : 12.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (room.isRoomAdmin)
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
foregroundColor:
|
||||
theme.colorScheme.onErrorContainer,
|
||||
backgroundColor:
|
||||
theme.colorScheme.errorContainer,
|
||||
),
|
||||
onPressed: () =>
|
||||
_finishActivity(forAll: true),
|
||||
child: Text(
|
||||
L10n.of(context).endForAll,
|
||||
style: TextStyle(
|
||||
fontSize: isColumnMode ? 16.0 : 12.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
if (_showDropdown)
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => _setShowDropdown(false),
|
||||
child: Container(color: Colors.black.withAlpha(100)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class ActivityStatsButton extends StatefulWidget {
|
||||
final ChatController controller;
|
||||
|
||||
const ActivityStatsButton({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ActivityStatsButton> createState() => _ActivityStatsButtonState();
|
||||
}
|
||||
|
||||
class _ActivityStatsButtonState extends State<ActivityStatsButton> {
|
||||
StreamSubscription? _analyticsSubscription;
|
||||
ActivitySummaryAnalyticsModel analytics = ActivitySummaryAnalyticsModel();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => _updateAnalytics(),
|
||||
);
|
||||
|
||||
_analyticsSubscription = widget
|
||||
.controller.pangeaController.getAnalytics.analyticsStream.stream
|
||||
.listen((_) {
|
||||
_updateAnalytics();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_analyticsSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
int get xpCount => analytics.totalXPForUser(
|
||||
Matrix.of(context).client.userID ?? '',
|
||||
);
|
||||
|
||||
int get vocabCount => analytics.uniqueConstructCountForUser(
|
||||
widget.controller.room.client.userID!,
|
||||
ConstructTypeEnum.vocab,
|
||||
);
|
||||
|
||||
int get grammarCount => analytics.uniqueConstructCountForUser(
|
||||
widget.controller.room.client.userID!,
|
||||
ConstructTypeEnum.morph,
|
||||
);
|
||||
|
||||
Future<void> _updateAnalytics() async {
|
||||
final analytics = await widget.controller.room.getActivityAnalytics();
|
||||
if (mounted) {
|
||||
setState(() => this.analytics = analytics);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 350,
|
||||
height: 55,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: () => widget.controller.setShowDropdown(
|
||||
!widget.controller.showActivityDropdown,
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppConfig.goldLight.withAlpha(100),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_StatsBadge(icon: Icons.radar, value: "$xpCount XP"),
|
||||
_StatsBadge(icon: Symbols.dictionary, value: "$vocabCount"),
|
||||
_StatsBadge(
|
||||
icon: Symbols.toys_and_games,
|
||||
value: "$grammarCount",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatsBadge extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String value;
|
||||
const _StatsBadge({
|
||||
required this.icon,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final baseStyle = theme.textTheme.bodyMedium;
|
||||
final double fontSize = (screenWidth < 400) ? 10 : 14;
|
||||
final double iconSize = (screenWidth < 400) ? 14 : 18;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: iconSize,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: baseStyle?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.onSurface,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,341 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_session_details_row.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
|
||||
class ActivityStatsMenu extends StatefulWidget {
|
||||
final ChatController controller;
|
||||
const ActivityStatsMenu(
|
||||
this.controller, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ActivityStatsMenu> createState() => ActivityStatsMenuState();
|
||||
}
|
||||
|
||||
class ActivityStatsMenuState extends State<ActivityStatsMenu> {
|
||||
ActivitySummaryAnalyticsModel? analytics;
|
||||
Room get room => widget.controller.room;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_updateUsedVocab();
|
||||
});
|
||||
}
|
||||
|
||||
Set<String>? get _usedVocab => analytics?.constructs[room.client.userID!]
|
||||
?.constructsOfType(ConstructTypeEnum.vocab)
|
||||
.map((id) => id.lemma)
|
||||
.toSet();
|
||||
|
||||
double get _percentVocabComplete {
|
||||
final vocabList = room.activityPlan?.vocabList ?? [];
|
||||
if (vocabList.isEmpty || _usedVocab == null) {
|
||||
return 0;
|
||||
}
|
||||
return _usedVocab!.intersection(vocabList.toSet()).length /
|
||||
vocabList.length;
|
||||
}
|
||||
|
||||
Future<void> _updateUsedVocab() async {
|
||||
final analytics = await room.getActivityAnalytics();
|
||||
if (mounted) {
|
||||
setState(() => this.analytics = analytics);
|
||||
}
|
||||
}
|
||||
|
||||
int _getAssignedRolesCount() {
|
||||
final assignedRoles = room.assignedRoles;
|
||||
if (assignedRoles == null) return 0;
|
||||
final nonBotRoles = assignedRoles.values.where(
|
||||
(role) => role.userId != BotName.byEnvironment,
|
||||
);
|
||||
|
||||
return nonBotRoles.length;
|
||||
}
|
||||
|
||||
int _getCompletedRolesCount() {
|
||||
final assignedRoles = room.assignedRoles;
|
||||
if (assignedRoles == null) return 0;
|
||||
|
||||
// Filter out the bot and count only finished non-bot roles
|
||||
return assignedRoles.values
|
||||
.where(
|
||||
(role) => role.userId != BotName.byEnvironment && role.isFinished,
|
||||
)
|
||||
.length;
|
||||
}
|
||||
|
||||
bool _isBotParticipant() {
|
||||
final assignedRoles = room.assignedRoles;
|
||||
if (assignedRoles == null) return false;
|
||||
return assignedRoles.values.any(
|
||||
(role) => role.userId == BotName.byEnvironment,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _finishActivity({bool forAll = false}) async {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
forAll
|
||||
? await room.finishActivityForAll()
|
||||
: await room.finishActivity();
|
||||
if (mounted) {
|
||||
widget.controller.setShowDropdown(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!room.showActivityChatUI) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final theme = Theme.of(context);
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
|
||||
// Completion status variables
|
||||
final bool userComplete = room.hasCompletedActivity;
|
||||
final bool activityComplete = room.activityIsFinished;
|
||||
bool shouldShowEndForAll = true;
|
||||
bool shouldShowImDone = true;
|
||||
String message = "";
|
||||
|
||||
if (!room.isRoomAdmin) {
|
||||
shouldShowEndForAll = false;
|
||||
}
|
||||
|
||||
//dont need endforall if only w bot
|
||||
if ((_getAssignedRolesCount() == 1) && (_isBotParticipant() == true)) {
|
||||
shouldShowEndForAll = false;
|
||||
}
|
||||
|
||||
if (activityComplete) {
|
||||
//activity is finished, no buttons
|
||||
shouldShowImDone = false;
|
||||
shouldShowEndForAll = false;
|
||||
message = L10n.of(context).activityComplete;
|
||||
} else {
|
||||
//activity is ongoing
|
||||
if (_getCompletedRolesCount() == 0 ||
|
||||
(_getAssignedRolesCount() == 1) && (_isBotParticipant() == true)) {
|
||||
//IF nobodys done or you're only playing with the bot,
|
||||
//Then it should show tips about your progress and nudge you to continue/end
|
||||
if ((_percentVocabComplete < .7) && (_usedVocab?.length ?? 0) < 50) {
|
||||
message = L10n.of(context).haventChattedMuch;
|
||||
} else {
|
||||
message = L10n.of(context).haveChatted;
|
||||
}
|
||||
} else {
|
||||
//user is in group with other users OR someone has wrapped up
|
||||
if (userComplete) {
|
||||
//user is done but group is ongoing, no buttons
|
||||
message = L10n.of(context).userDoneAndWaiting(
|
||||
_getCompletedRolesCount(),
|
||||
_getAssignedRolesCount(),
|
||||
);
|
||||
} else {
|
||||
//user is not done, buttons are present
|
||||
message = L10n.of(context).othersDoneAndWaiting(
|
||||
_getCompletedRolesCount(),
|
||||
_getAssignedRolesCount(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: widget.controller.showActivityDropdown ? 0 : null,
|
||||
child: Column(
|
||||
children: [
|
||||
ClipRect(
|
||||
child: AnimatedAlign(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: Curves.easeInOut,
|
||||
heightFactor: widget.controller.showActivityDropdown ? 1.0 : 0.0,
|
||||
alignment: Alignment.topCenter,
|
||||
child: GestureDetector(
|
||||
onPanUpdate: (details) {
|
||||
if (details.delta.dy < -2) {
|
||||
widget.controller.setShowDropdown(false);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
),
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
spacing: 12.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Column(
|
||||
spacing: 8.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ActivitySessionDetailsRow(
|
||||
icon: Symbols.radar,
|
||||
iconSize: 16.0,
|
||||
child: Text(
|
||||
room.activityPlan!.learningObjective,
|
||||
style: const TextStyle(fontSize: 12.0),
|
||||
),
|
||||
),
|
||||
ActivitySessionDetailsRow(
|
||||
icon: Symbols.dictionary,
|
||||
iconSize: 16.0,
|
||||
child: Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 4.0,
|
||||
children: [
|
||||
...room.activityPlan!.vocabList.map(
|
||||
(vocabWord) => VocabTile(
|
||||
vocabWord: vocabWord,
|
||||
isUsed:
|
||||
(_usedVocab ?? {}).contains(vocabWord),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
message,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (!userComplete) ...[
|
||||
if (shouldShowEndForAll)
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
side: BorderSide(
|
||||
color: theme.colorScheme.secondaryContainer,
|
||||
width: 2,
|
||||
),
|
||||
foregroundColor: theme.colorScheme.primary,
|
||||
backgroundColor: theme.colorScheme.surface,
|
||||
),
|
||||
onPressed: () => _finishActivity(forAll: true),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).endForAll,
|
||||
style: TextStyle(
|
||||
fontSize: isColumnMode ? 16.0 : 12.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (shouldShowImDone)
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
),
|
||||
onPressed: _finishActivity,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).endActivityTitle,
|
||||
style: TextStyle(
|
||||
fontSize: isColumnMode ? 16.0 : 12.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.controller.showActivityDropdown)
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => widget.controller.setShowDropdown(false),
|
||||
child: Container(color: Colors.black.withAlpha(100)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VocabTile extends StatelessWidget {
|
||||
final String vocabWord;
|
||||
final bool isUsed;
|
||||
|
||||
const VocabTile({
|
||||
super.key,
|
||||
required this.vocabWord,
|
||||
required this.isUsed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color =
|
||||
isUsed ? AppConfig.goldLight.withAlpha(100) : Colors.transparent;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
vocabWord,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,17 @@ class ActivitySummaryAnalyticsModel {
|
|||
return userAnalytics.constructsOfType(type).length;
|
||||
}
|
||||
|
||||
int totalXPForUser(String userId) {
|
||||
final userAnalytics = constructs[userId];
|
||||
if (userAnalytics == null) return 0;
|
||||
|
||||
int totalXP = 0;
|
||||
for (final usage in userAnalytics.usages.values) {
|
||||
totalXP += usage.timesUsed;
|
||||
}
|
||||
return totalXP;
|
||||
}
|
||||
|
||||
void addConstructs(PangeaMessageEvent event) {
|
||||
final uses = event.originalSent?.vocabAndMorphUses();
|
||||
if (uses == null || uses.isEmpty) return;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat_details/chat_details.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/pages/room_details_buttons.dart';
|
||||
|
|
@ -128,13 +129,15 @@ class ChatDetailsButtonRowState extends State<ChatDetailsButtonRow> {
|
|||
onSubmit: widget.controller.setBotOptions,
|
||||
),
|
||||
),
|
||||
visible: !room.isDirectChat || room.botOptions != null,
|
||||
visible: (!room.isDirectChat || room.botOptions != null) &&
|
||||
!room.showActivityChatUI,
|
||||
enabled: room.canInvite,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.chatCapacity,
|
||||
icon: const Icon(Icons.reduce_capacity, size: 30.0),
|
||||
onPressed: widget.controller.setRoomCapacity,
|
||||
visible: !room.showActivityChatUI,
|
||||
enabled: !room.isDirectChat && room.canSendDefaultStates,
|
||||
showInMainView: false,
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue