refactor: move activity vocab into widget, move activity-related noti… (#4694)

* refactor: move activity vocab into widget, move activity-related notifiers from chat controller to their own controller

* Update lib/pangea/activity_sessions/activity_session_chat/activity_vocab_widget.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update lib/pangea/activity_sessions/activity_session_chat/activity_chat_controller.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* remove unused value notifiers, add error handling to analytics update function

* reduce duplicate code

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
ggurdin 2025-11-19 15:09:49 -05:00 committed by GitHub
parent e5852b5704
commit 8ae30303b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 307 additions and 288 deletions

View file

@ -27,8 +27,8 @@ import 'package:fluffychat/pages/chat/event_info_dialog.dart';
import 'package:fluffychat/pages/chat/events/audio_player.dart';
import 'package:fluffychat/pages/chat/recording_dialog.dart';
import 'package:fluffychat/pages/chat_details/chat_details.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_chat_controller.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart';
@ -188,6 +188,7 @@ class ChatController extends State<ChatPageWithRoom>
StreamSubscription? _analyticsSubscription;
StreamSubscription? _botAudioSubscription;
final timelineUpdateNotifier = _TimelineUpdateNotifier();
late final ActivityChatController activityController;
// Pangea#
Room get room => sendingClient.getRoomById(roomId) ?? widget.room;
@ -536,6 +537,11 @@ class ChatController extends State<ChatPageWithRoom>
_botAudioSubscription = room.client.onSync.stream.listen(_botAudioListener);
activityController = ActivityChatController(
userID: Matrix.of(context).client.userID!,
getAnalytics: room.getActivityAnalytics,
);
Future.delayed(const Duration(seconds: 1), () async {
if (!mounted) return;
pangeaController.languageController.showDialogOnEmptyLanguage(
@ -782,14 +788,11 @@ class ChatController extends State<ChatPageWithRoom>
_storeInputTimeoutTimer?.cancel();
_displayChatDetailsColumn.dispose();
timelineUpdateNotifier.dispose();
highlightedRole.dispose();
showInstructions.dispose();
showActivityDropdown.dispose();
hasRainedConfetti.dispose();
typingCoolDown?.cancel();
typingTimeout?.cancel();
scrollController.removeListener(_updateScrollController);
choreographer.dispose();
activityController.dispose();
MatrixState.pAnyState.closeAllOverlays(force: true);
showToolbarStream.close();
stopMediaStream.close();
@ -797,7 +800,6 @@ class ChatController extends State<ChatPageWithRoom>
_analyticsSubscription?.cancel();
_botAudioSubscription?.cancel();
_router.routeInformationProvider.removeListener(_onRouteChanged);
carouselController.dispose();
scrollController.dispose();
inputFocus.dispose();
TokensUtil.clearNewTokenCache();
@ -2316,33 +2318,9 @@ class ChatController extends State<ChatPageWithRoom>
}
}
final ScrollController carouselController = ScrollController();
ValueNotifier<ActivityRoleModel?> highlightedRole = ValueNotifier(null);
void highlightRole(ActivityRoleModel role) {
if (mounted) highlightedRole.value = role;
}
ValueNotifier<bool> showInstructions = ValueNotifier(false);
void toggleShowInstructions() {
if (mounted) {
showInstructions.value = !showInstructions.value;
}
}
ValueNotifier<bool> showActivityDropdown = ValueNotifier(false);
void toggleShowDropdown() async {
if (mounted) {
inputFocus.unfocus();
showActivityDropdown.value = !showActivityDropdown.value;
}
}
ValueNotifier<bool> hasRainedConfetti = ValueNotifier(false);
void setHasRainedConfetti(bool show) {
if (mounted) {
hasRainedConfetti.value = show;
}
void toggleShowDropdown() {
inputFocus.unfocus();
activityController.toggleShowDropdown();
}
// Pangea#

View file

@ -497,14 +497,16 @@ class ChatView extends StatelessWidget {
ActivityStatsMenu(controller),
if (controller.room.activitySummary?.summary != null)
ValueListenableBuilder(
valueListenable: controller.hasRainedConfetti,
valueListenable:
controller.activityController.hasRainedConfetti,
builder: (context, hasRained, __) {
return hasRained
? const SizedBox()
: StarRainWidget(
showBlast: true,
onFinished: () =>
controller.setHasRainedConfetti(true),
onFinished: () => controller
.activityController
.setHasRainedConfetti(true),
);
},
),

View file

@ -138,7 +138,7 @@ class Message extends StatelessWidget {
if (event.type == PangeaEventTypes.activityPlan &&
event.room.activityPlan != null) {
return ValueListenableBuilder(
valueListenable: controller.showInstructions,
valueListenable: controller.activityController.showInstructions,
builder: (context, show, __) {
return ActivitySummary(
activity: event.room.activityPlan!,
@ -147,11 +147,13 @@ class Message extends StatelessWidget {
? event.room.activityRoles?.roles ?? {}
: event.room.assignedRoles ?? {},
showInstructions: show,
toggleInstructions: controller.toggleShowInstructions,
toggleInstructions:
controller.activityController.toggleShowInstructions,
getParticipantOpacity: (role) =>
role == null || role.isFinished ? 0.5 : 1.0,
isParticipantSelected: (id) =>
controller.room.ownRoleState?.id == id,
usedVocab: controller.activityController.usedVocab,
);
},
);

View file

@ -0,0 +1,100 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_role_model.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/common/utils/error_handler.dart';
import 'package:fluffychat/widgets/matrix.dart';
class ActivityChatController {
final String userID;
final Future<ActivitySummaryAnalyticsModel> Function()? getAnalytics;
ActivityChatController({
required this.userID,
required this.getAnalytics,
}) {
init();
}
StreamSubscription? _analyticsSubscription;
bool _disposed = false;
final ScrollController carouselController = ScrollController();
final ValueNotifier<Set<String>> usedVocab = ValueNotifier({});
final ValueNotifier<ActivityRoleModel?> highlightedRole = ValueNotifier(null);
final ValueNotifier<bool> showInstructions = ValueNotifier(false);
final ValueNotifier<bool> showActivityDropdown = ValueNotifier(false);
final ValueNotifier<bool> hasRainedConfetti = ValueNotifier(false);
void init() {
if (getAnalytics != null) {
_updateUsedVocab();
_analyticsSubscription = MatrixState
.pangeaController.getAnalytics.analyticsStream.stream
.listen((_) {
_updateUsedVocab();
});
}
}
void dispose() {
_disposed = true;
carouselController.dispose();
_analyticsSubscription?.cancel();
usedVocab.dispose();
highlightedRole.dispose();
showInstructions.dispose();
showActivityDropdown.dispose();
hasRainedConfetti.dispose();
}
void highlightRole(ActivityRoleModel role) {
if (!_disposed) {
highlightedRole.value = role;
}
}
void toggleShowInstructions() {
if (!_disposed) {
showInstructions.value = !showInstructions.value;
}
}
void toggleShowDropdown() {
if (!_disposed) {
showActivityDropdown.value = !showActivityDropdown.value;
}
}
void setHasRainedConfetti(bool show) {
if (!_disposed) {
hasRainedConfetti.value = show;
}
}
Future<void> _updateUsedVocab() async {
if (getAnalytics == null) return;
try {
final analytics = await getAnalytics!.call();
if (!_disposed) {
usedVocab.value = analytics.constructs[userID]
?.constructsOfType(ConstructTypeEnum.vocab)
.map((id) => id.lemma.toLowerCase())
.toSet() ??
{};
}
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
"message": "Failed to update used vocab in ActivityChatController",
},
);
}
}
}

View file

@ -3,77 +3,26 @@ 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_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_vocab_widget.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/common/utils/overlay.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
class ActivityStatsMenu extends StatefulWidget {
class ActivityStatsMenu extends StatelessWidget {
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;
StreamSubscription? _analyticsSubscription;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_updateUsedVocab();
});
_analyticsSubscription = widget
.controller.pangeaController.getAnalytics.analyticsStream.stream
.listen((_) {
_updateUsedVocab();
});
}
@override
void dispose() {
_analyticsSubscription?.cancel();
super.dispose();
}
Set<String>? get _usedVocab => analytics?.constructs[room.client.userID!]
?.constructsOfType(ConstructTypeEnum.vocab)
.map((id) => id.lemma.toLowerCase())
.toSet();
Future<void> _updateUsedVocab() async {
final analytics = await room.getActivityAnalytics();
if (mounted) {
setState(() => this.analytics = analytics);
}
}
int _getAssignedRolesCount() {
final assignedRoles = room.assignedRoles;
final assignedRoles = controller.room.assignedRoles;
if (assignedRoles == null) return 0;
final nonBotRoles = assignedRoles.values.where(
(role) => role.userId != BotName.byEnvironment,
@ -83,30 +32,31 @@ class ActivityStatsMenuState extends State<ActivityStatsMenu> {
}
bool _isBotParticipant() {
final assignedRoles = room.assignedRoles;
final assignedRoles = controller.room.assignedRoles;
if (assignedRoles == null) return false;
return assignedRoles.values.any(
(role) => role.userId == BotName.byEnvironment,
);
}
Future<void> _finishActivity({bool forAll = false}) async {
Future<void> _finishActivity(
BuildContext context, {
bool forAll = false,
}) async {
await showFutureLoadingDialog(
context: context,
future: () async {
forAll
? await room.finishActivityForAll()
: await room.finishActivity();
if (mounted) {
widget.controller.toggleShowDropdown();
}
? await controller.room.finishActivityForAll()
: await controller.room.finishActivity();
controller.toggleShowDropdown();
},
);
}
@override
Widget build(BuildContext context) {
if (!room.showActivityChatUI) {
if (!controller.room.showActivityChatUI) {
return const SizedBox.shrink();
}
@ -114,12 +64,12 @@ class ActivityStatsMenuState extends State<ActivityStatsMenu> {
final isColumnMode = FluffyThemes.isColumnMode(context);
// Completion status variables
final bool userComplete = room.hasCompletedRole;
final bool activityComplete = room.isActivityFinished;
final bool userComplete = controller.room.hasCompletedRole;
final bool activityComplete = controller.room.isActivityFinished;
bool shouldShowEndForAll = true;
bool shouldShowImDone = true;
if (!room.isRoomAdmin) {
if (!controller.room.isRoomAdmin) {
shouldShowEndForAll = false;
}
@ -135,7 +85,7 @@ class ActivityStatsMenuState extends State<ActivityStatsMenu> {
}
return ValueListenableBuilder(
valueListenable: widget.controller.showActivityDropdown,
valueListenable: controller.activityController.showActivityDropdown,
builder: (context, showDropdown, child) {
return Positioned(
top: 0,
@ -153,7 +103,7 @@ class ActivityStatsMenuState extends State<ActivityStatsMenu> {
child: GestureDetector(
onPanUpdate: (details) {
if (details.delta.dy < -2) {
widget.controller.toggleShowDropdown();
controller.toggleShowDropdown();
}
},
child: child,
@ -163,7 +113,7 @@ class ActivityStatsMenuState extends State<ActivityStatsMenu> {
if (showDropdown)
Expanded(
child: GestureDetector(
onTap: widget.controller.toggleShowDropdown,
onTap: controller.toggleShowDropdown,
child: Container(color: Colors.black.withAlpha(100)),
),
),
@ -189,26 +139,21 @@ class ActivityStatsMenuState extends State<ActivityStatsMenu> {
icon: Symbols.radar,
iconSize: 16.0,
child: Text(
room.activityPlan!.learningObjective,
controller.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!.vocab.map(
(v) => VocabTile(
vocab: v,
langCode: room.activityPlan!.req.targetLanguage,
isUsed: (_usedVocab ?? {})
.contains(v.lemma.toLowerCase()),
),
),
],
child: ActivityVocabWidget(
key: ValueKey(
"activity-stats-${controller.room.activityPlan!.activityId}",
),
vocab: controller.room.activityPlan!.vocab,
langCode: controller.room.activityPlan!.req.targetLanguage,
targetId: "activity-vocab",
usedVocab: controller.activityController.usedVocab,
),
),
],
@ -236,7 +181,7 @@ class ActivityStatsMenuState extends State<ActivityStatsMenu> {
foregroundColor: theme.colorScheme.primary,
backgroundColor: theme.colorScheme.surface,
),
onPressed: () => _finishActivity(forAll: true),
onPressed: () => _finishActivity(context, forAll: true),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@ -257,7 +202,7 @@ class ActivityStatsMenuState extends State<ActivityStatsMenu> {
vertical: 8.0,
),
),
onPressed: _finishActivity,
onPressed: () => _finishActivity(context),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@ -277,89 +222,3 @@ class ActivityStatsMenuState extends State<ActivityStatsMenu> {
);
}
}
class VocabTile extends StatelessWidget {
final Vocab vocab;
final String langCode;
final bool isUsed;
const VocabTile({
super.key,
required this.vocab,
required this.langCode,
required this.isUsed,
});
@override
Widget build(BuildContext context) {
final color = isUsed
? Color.alphaBlend(
Theme.of(context).colorScheme.surface.withAlpha(150),
AppConfig.gold,
)
: Colors.transparent;
return CompositedTransformTarget(
link: MatrixState.pAnyState
.layerLinkAndKey(
"activity-vocab-${vocab.lemma}",
)
.link,
child: InkWell(
key: MatrixState.pAnyState
.layerLinkAndKey(
"activity-vocab-${vocab.lemma}",
)
.key,
borderRadius: BorderRadius.circular(
24.0,
),
onTap: () {
OverlayUtil.showPositionedCard(
overlayKey: "activity-vocab-${vocab.lemma}",
context: context,
cardToShow: WordZoomWidget(
token: PangeaTokenText(
content: vocab.lemma,
length: vocab.lemma.characters.length,
offset: 0,
),
construct: ConstructIdentifier(
lemma: vocab.lemma,
type: ConstructTypeEnum.vocab,
category: vocab.pos,
),
langCode: langCode,
onClose: () {
MatrixState.pAnyState.closeOverlay(
"activity-vocab-${vocab.lemma}",
);
},
),
transformTargetId: "activity-vocab-${vocab.lemma}",
closePrevOverlay: false,
addBorder: false,
maxWidth: AppConfig.toolbarMinWidth,
maxHeight: AppConfig.toolbarMaxHeight,
);
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 4.0,
),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(20),
),
child: Text(
vocab.lemma,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontSize: 14.0,
),
),
),
),
);
}
}

View file

@ -0,0 +1,141 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/common/utils/overlay.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart';
import 'package:fluffychat/widgets/matrix.dart';
class ActivityVocabWidget extends StatelessWidget {
final List<Vocab> vocab;
final String langCode;
final String targetId;
final ValueNotifier<Set<String>>? usedVocab;
const ActivityVocabWidget({
super.key,
required this.vocab,
required this.langCode,
required this.targetId,
this.usedVocab,
});
@override
Widget build(BuildContext context) {
if (usedVocab == null) {
return _VocabChips(
vocab: vocab,
targetId: targetId,
langCode: langCode,
usedVocab: const {},
);
}
return ValueListenableBuilder(
valueListenable: usedVocab!,
builder: (context, used, __) => _VocabChips(
vocab: vocab,
targetId: targetId,
langCode: langCode,
usedVocab: used,
),
);
}
}
class _VocabChips extends StatelessWidget {
final List<Vocab> vocab;
final String targetId;
final String langCode;
final Set<String> usedVocab;
const _VocabChips({
required this.vocab,
required this.targetId,
required this.langCode,
required this.usedVocab,
});
void _onTap(Vocab v, BuildContext context) {
final target = "$targetId-${v.lemma}";
OverlayUtil.showPositionedCard(
overlayKey: target,
context: context,
cardToShow: WordZoomWidget(
token: PangeaTokenText(
content: v.lemma,
length: v.lemma.characters.length,
offset: 0,
),
construct: ConstructIdentifier(
lemma: v.lemma,
type: ConstructTypeEnum.vocab,
category: v.pos,
),
langCode: langCode,
onClose: () {
MatrixState.pAnyState.closeOverlay(target);
},
),
transformTargetId: target,
closePrevOverlay: false,
addBorder: false,
maxWidth: AppConfig.toolbarMinWidth,
maxHeight: AppConfig.toolbarMaxHeight,
);
}
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 4.0,
runSpacing: 4.0,
children: [
...vocab.map(
(v) {
final target = "$targetId-${v.lemma}";
final color = usedVocab.contains(v.lemma.toLowerCase())
? Color.alphaBlend(
Theme.of(context).colorScheme.surface.withAlpha(150),
AppConfig.gold,
)
: Theme.of(context).colorScheme.primary.withAlpha(20);
final linkAndKey = MatrixState.pAnyState.layerLinkAndKey(target);
return CompositedTransformTarget(
link: linkAndKey.link,
child: InkWell(
key: linkAndKey.key,
borderRadius: BorderRadius.circular(
24.0,
),
onTap: () => _onTap(v, context),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 4.0,
),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(20),
),
child: Text(
v.lemma,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontSize: 14.0,
),
),
),
),
);
},
),
],
);
}
}

View file

@ -9,21 +9,15 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:matrix/matrix.dart';
import 'package:matrix/src/utils/markdown.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_participant_list.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_vocab_widget.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_details_row.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/common/utils/overlay.dart';
import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart';
import 'package:fluffychat/widgets/matrix.dart';
class ActivitySummary extends StatelessWidget {
final ActivityPlanModel activity;
@ -39,12 +33,15 @@ class ActivitySummary extends StatelessWidget {
final bool Function(String)? isParticipantSelected;
final double Function(ActivityRoleModel?)? getParticipantOpacity;
final ValueNotifier<Set<String>>? usedVocab;
const ActivitySummary({
super.key,
required this.activity,
required this.showInstructions,
required this.toggleInstructions,
required this.assignedRoles,
this.usedVocab,
this.onTapParticipant,
this.canSelectParticipant,
this.isParticipantSelected,
@ -183,75 +180,14 @@ class ActivitySummary extends StatelessWidget {
ActivitySessionDetailsRow(
icon: Symbols.dictionary,
iconSize: 16.0,
child: Wrap(
spacing: 4.0,
runSpacing: 4.0,
children: activity.vocab.map((vocab) {
return CompositedTransformTarget(
link: MatrixState.pAnyState
.layerLinkAndKey(
"activity-summary-vocab-${vocab.lemma}",
)
.link,
child: InkWell(
key: MatrixState.pAnyState
.layerLinkAndKey(
"activity-summary-vocab-${vocab.lemma}",
)
.key,
borderRadius: BorderRadius.circular(
24.0,
),
onTap: () {
OverlayUtil.showPositionedCard(
overlayKey:
"activity-summary-vocab-${vocab.lemma}",
context: context,
cardToShow: WordZoomWidget(
token: PangeaTokenText(
content: vocab.lemma,
length: vocab.lemma.characters.length,
offset: 0,
),
construct: ConstructIdentifier(
lemma: vocab.lemma,
type: ConstructTypeEnum.vocab,
category: vocab.pos,
),
langCode: activity.req.targetLanguage,
onClose: () {
MatrixState.pAnyState.closeOverlay(
"activity-summary-vocab-${vocab.lemma}",
);
},
),
transformTargetId:
"activity-summary-vocab-${vocab.lemma}",
closePrevOverlay: false,
addBorder: false,
maxWidth: AppConfig.toolbarMinWidth,
maxHeight: AppConfig.toolbarMaxHeight,
);
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 4.0,
),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withAlpha(
20,
),
borderRadius: BorderRadius.circular(20),
),
child: Text(
vocab.lemma,
style: theme.textTheme.bodyMedium,
),
),
),
);
}).toList(),
child: ActivityVocabWidget(
key: ValueKey(
"activity-summary-${activity.activityId}",
),
vocab: activity.vocab,
langCode: activity.req.targetLanguage,
targetId: "activity-summary-vocab",
usedVocab: usedVocab,
),
),
],

View file

@ -110,7 +110,7 @@ class ButtonControlledCarouselView extends StatelessWidget {
height: 270.0,
child: ListView(
shrinkWrap: true,
controller: controller.carouselController,
controller: controller.activityController.carouselController,
scrollDirection: Axis.horizontal,
children: userSummaries.mapIndexed((i, p) {
final user = room.getParticipants().firstWhereOrNull(
@ -231,7 +231,7 @@ class ButtonControlledCarouselView extends StatelessWidget {
),
const SizedBox(height: 12),
ValueListenableBuilder(
valueListenable: controller.highlightedRole,
valueListenable: controller.activityController.highlightedRole,
builder: (context, highlightedRole, __) {
return Row(
mainAxisSize: MainAxisSize.min,
@ -250,8 +250,9 @@ class ButtonControlledCarouselView extends StatelessWidget {
borderRadius: BorderRadius.circular(4),
selected: highlightedRole?.id == userRole.id,
onTap: () {
controller.highlightRole(userRole);
controller.carouselController.jumpTo(i * 250.0);
controller.activityController.highlightRole(userRole);
controller.activityController.carouselController
.jumpTo(i * 250.0);
},
);
}).toList(),