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:
parent
e5852b5704
commit
8ae30303b1
8 changed files with 307 additions and 288 deletions
|
|
@ -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#
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue