feat: activity dropdown menu
parent
5ce2a787b4
commit
b45541d826
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue