refactor: replace activity stats menu with smaller button
This commit is contained in:
parent
8d69de4586
commit
ff21ab8608
9 changed files with 204 additions and 316 deletions
|
|
@ -5317,5 +5317,6 @@
|
|||
"feedbackDialogDesc": "I make mistakes too! Anything to help me improve?",
|
||||
"getStartedFriendsButton": "Invite a friend",
|
||||
"contactHasBeenInvitedToTheCourse": "Contact has been invited to the course",
|
||||
"inviteFriends": "Invite friends"
|
||||
"inviteFriends": "Invite friends",
|
||||
"activityStatsButtonTooltip": "Activity info"
|
||||
}
|
||||
|
|
@ -2285,8 +2285,8 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
}
|
||||
|
||||
bool showActivityDropdown = false;
|
||||
void setShowDropdown(bool show) async {
|
||||
setState(() => showActivityDropdown = show);
|
||||
void toggleShowDropdown() async {
|
||||
setState(() => showActivityDropdown = !showActivityDropdown);
|
||||
}
|
||||
|
||||
bool hasRainedConfetti = false;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ 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';
|
||||
|
|
@ -30,23 +28,6 @@ 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,6 +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_menu_button.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/analytics_misc/level_up/star_rain_widget.dart';
|
||||
|
|
@ -147,6 +148,8 @@ class ChatView extends StatelessWidget {
|
|||
context.go('/rooms/${controller.room.id}/search');
|
||||
},
|
||||
),
|
||||
if (controller.room.showActivityChatUI)
|
||||
ActivityMenuButton(controller: controller),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
tooltip: L10n.of(context).chatDetails,
|
||||
|
|
@ -216,9 +219,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
extension ActivityMenuLogic on ChatController {
|
||||
bool get shouldShowActivityInstructions {
|
||||
if (InstructionsEnum.activityStatsMenu.isToggledOff ||
|
||||
MatrixState.pAnyState.isOverlayOpen(RegExp(r"^word-zoom-card-.*$")) ||
|
||||
timeline == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final userID = Matrix.of(context).client.userID!;
|
||||
final activityRoles = room.activityRoles?.roles.values ?? [];
|
||||
final finishedRoles = activityRoles.where((r) => r.isFinished);
|
||||
|
||||
if (finishedRoles.isNotEmpty) {
|
||||
return !finishedRoles.any((r) => r.userId == userID);
|
||||
}
|
||||
|
||||
final count = timeline!.events
|
||||
.where(
|
||||
(event) =>
|
||||
event.senderId == userID &&
|
||||
event.type == EventTypes.Message &&
|
||||
{MessageTypes.Text, MessageTypes.Audio}
|
||||
.contains(event.messageType),
|
||||
)
|
||||
.length;
|
||||
|
||||
return count >= 3;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_chat_extension.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/overlay.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/tutorial_overlay_message.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
|
||||
|
||||
class ActivityMenuButton extends StatefulWidget {
|
||||
final ChatController controller;
|
||||
|
||||
const ActivityMenuButton({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ActivityMenuButton> createState() => _ActivityMenuButtonState();
|
||||
}
|
||||
|
||||
class _ActivityMenuButtonState extends State<ActivityMenuButton> {
|
||||
bool _showShimmer = false;
|
||||
StreamSubscription? _rolesSubscription;
|
||||
StreamSubscription? _analyticsSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_analyticsSubscription = widget
|
||||
.controller.pangeaController.getAnalytics.analyticsStream.stream
|
||||
.listen(_showStatsMenuDropdownInstructions);
|
||||
|
||||
_rolesSubscription = widget.controller.room.client.onRoomState.stream
|
||||
.where(
|
||||
(u) =>
|
||||
u.roomId == widget.controller.room.id &&
|
||||
u.state.type == PangeaEventTypes.activityRole,
|
||||
)
|
||||
.listen(_showStatsMenuDropdownInstructions);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_analyticsSubscription?.cancel();
|
||||
_rolesSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Show a tutorial overlay that blocks the screen and points
|
||||
/// to the stats menu button with an explanation of what it does.
|
||||
void _showStatsMenuDropdownInstructions(_) {
|
||||
if (!mounted) return;
|
||||
if (!widget.controller.shouldShowActivityInstructions) {
|
||||
return;
|
||||
}
|
||||
|
||||
final renderObject = context.findRenderObject() as RenderBox;
|
||||
final offset = renderObject.localToGlobal(Offset.zero);
|
||||
final cellRect = Rect.fromLTWH(
|
||||
offset.dx,
|
||||
offset.dy,
|
||||
renderObject.size.width,
|
||||
renderObject.size.height,
|
||||
);
|
||||
|
||||
OverlayUtil.showTutorialOverlay(
|
||||
context,
|
||||
overlayContent: TutorialOverlayMessage(
|
||||
L10n.of(context).activityStatsButtonInstruction,
|
||||
),
|
||||
overlayKey: "activity_stats_menu_instruction",
|
||||
anchorRect: cellRect,
|
||||
borderRadius: 12.0,
|
||||
padding: 8.0,
|
||||
onClick: () {
|
||||
setState(() => _showShimmer = false);
|
||||
InstructionsEnum.activityStatsMenu.setToggledOff(true);
|
||||
widget.controller.toggleShowDropdown();
|
||||
},
|
||||
);
|
||||
setState(() => _showShimmer = true);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final content = IconButton(
|
||||
icon: const Icon(Icons.radar_outlined),
|
||||
tooltip: L10n.of(context).activityStatsButtonTooltip,
|
||||
onPressed: widget.controller.toggleShowDropdown,
|
||||
);
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: _showShimmer
|
||||
? Shimmer.fromColors(
|
||||
enabled: _showShimmer,
|
||||
baseColor: Theme.of(context).iconTheme.color!,
|
||||
highlightColor: AppConfig.gold,
|
||||
child: content,
|
||||
)
|
||||
: content,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,288 +0,0 @@
|
|||
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_summary/activity_summary_analytics_model.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/pressable_button.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_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;
|
||||
StreamSubscription? _rolesSubscription;
|
||||
ActivitySummaryAnalyticsModel? analytics;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => _updateAnalytics(),
|
||||
);
|
||||
|
||||
_analyticsSubscription = widget
|
||||
.controller.pangeaController.getAnalytics.analyticsStream.stream
|
||||
.listen((_) => _updateAnalytics());
|
||||
|
||||
_rolesSubscription = widget.controller.room.client.onRoomState.stream
|
||||
.where(
|
||||
(u) =>
|
||||
u.roomId == widget.controller.room.id &&
|
||||
u.state.type == PangeaEventTypes.activityRole,
|
||||
)
|
||||
.listen((_) {
|
||||
_showStatsMenuDropdownInstructions();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_analyticsSubscription?.cancel();
|
||||
_rolesSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Client get _client => widget.controller.room.client;
|
||||
|
||||
bool get _shouldShowInstructions {
|
||||
if (InstructionsEnum.activityStatsMenu.isToggledOff ||
|
||||
MatrixState.pAnyState.isOverlayOpen(
|
||||
RegExp(r"^word-zoom-card-.*$"),
|
||||
) ||
|
||||
widget.controller.timeline == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if someone has finished the activity, enable the tooltip
|
||||
final activityRoles =
|
||||
widget.controller.room.activityRoles?.roles.values.toList() ?? [];
|
||||
final finishedRoles = activityRoles.where((r) => r.isFinished).toList();
|
||||
|
||||
if (finishedRoles.isNotEmpty) {
|
||||
return !finishedRoles.any((r) => r.userId == _client.userID);
|
||||
}
|
||||
|
||||
// otherwise, if no one has finished, only show if the user has sent >= 3 messages
|
||||
int count = 0;
|
||||
for (final event in widget.controller.timeline!.events) {
|
||||
if (event.senderId == _client.userID &&
|
||||
event.type == EventTypes.Message &&
|
||||
[
|
||||
MessageTypes.Text,
|
||||
MessageTypes.Audio,
|
||||
].contains(event.messageType)) {
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count >= 3) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int get _xpCount =>
|
||||
analytics?.totalXPForUser(
|
||||
_client.userID!,
|
||||
) ??
|
||||
0;
|
||||
|
||||
int? get _vocabCount => analytics?.uniqueConstructCountForUser(
|
||||
_client.userID!,
|
||||
ConstructTypeEnum.vocab,
|
||||
);
|
||||
|
||||
int? get _grammarCount => analytics?.uniqueConstructCountForUser(
|
||||
_client.userID!,
|
||||
ConstructTypeEnum.morph,
|
||||
);
|
||||
|
||||
/// Show a tutorial overlay that blocks the screen and points
|
||||
/// to the stats menu button with an explanation of what it does.
|
||||
void _showStatsMenuDropdownInstructions() {
|
||||
if (!_shouldShowInstructions) {
|
||||
return;
|
||||
}
|
||||
|
||||
final renderObject = context.findRenderObject() as RenderBox;
|
||||
final offset = renderObject.localToGlobal(Offset.zero);
|
||||
|
||||
final cellRect = Rect.fromLTWH(
|
||||
offset.dx,
|
||||
offset.dy,
|
||||
renderObject.size.width,
|
||||
renderObject.size.height,
|
||||
);
|
||||
|
||||
OverlayUtil.showTutorialOverlay(
|
||||
context,
|
||||
overlayContent: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
width: 200,
|
||||
alignment: Alignment.center,
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
children: [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Icon(
|
||||
Icons.info_outlined,
|
||||
size: 16.0,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
),
|
||||
const WidgetSpan(child: SizedBox(width: 4.0)),
|
||||
TextSpan(
|
||||
text: L10n.of(context).activityStatsButtonInstruction,
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
overlayKey: "activity_stats_menu_instruction",
|
||||
anchorRect: cellRect,
|
||||
borderRadius: 12.0,
|
||||
padding: 8.0,
|
||||
onClick: () {
|
||||
InstructionsEnum.activityStatsMenu.setToggledOff(true);
|
||||
widget.controller.setShowDropdown(true);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _updateAnalytics() async {
|
||||
final analytics = await widget.controller.room.getActivityAnalytics();
|
||||
if (mounted) {
|
||||
setState(() => this.analytics = analytics);
|
||||
_showStatsMenuDropdownInstructions();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final roleState =
|
||||
widget.controller.room.activityRoles?.roles.values.toList() ?? [];
|
||||
|
||||
final enabled = _xpCount > 0 || roleState.any((r) => r.isFinished);
|
||||
|
||||
return PressableButton(
|
||||
onPressed: () => widget.controller.setShowDropdown(
|
||||
!widget.controller.showActivityDropdown,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: enabled
|
||||
? Color.alphaBlend(
|
||||
Theme.of(context).colorScheme.surface.withAlpha(70),
|
||||
AppConfig.gold,
|
||||
)
|
||||
: theme.colorScheme.surface,
|
||||
depressed: !enabled || widget.controller.showActivityDropdown,
|
||||
child: AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
width: 280,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: enabled
|
||||
? Color.alphaBlend(
|
||||
Theme.of(context).colorScheme.surface.withAlpha(70),
|
||||
AppConfig.gold,
|
||||
)
|
||||
: theme.colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: analytics == null
|
||||
? const CircularProgressIndicator.adaptive()
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_StatsBadge(
|
||||
icon: Icons.star,
|
||||
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 baseStyle = theme.textTheme.bodyMedium;
|
||||
final double fontSize = FluffyThemes.isColumnMode(context) ? 18 : 14;
|
||||
final double iconSize = FluffyThemes.isColumnMode(context) ? 22 : 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -98,7 +98,7 @@ class ActivityStatsMenuState extends State<ActivityStatsMenu> {
|
|||
? await room.finishActivityForAll()
|
||||
: await room.finishActivity();
|
||||
if (mounted) {
|
||||
widget.controller.setShowDropdown(false);
|
||||
widget.controller.toggleShowDropdown();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -150,7 +150,7 @@ class ActivityStatsMenuState extends State<ActivityStatsMenu> {
|
|||
child: GestureDetector(
|
||||
onPanUpdate: (details) {
|
||||
if (details.delta.dy < -2) {
|
||||
widget.controller.setShowDropdown(false);
|
||||
widget.controller.toggleShowDropdown();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
|
|
@ -263,7 +263,7 @@ class ActivityStatsMenuState extends State<ActivityStatsMenu> {
|
|||
if (widget.controller.showActivityDropdown)
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => widget.controller.setShowDropdown(false),
|
||||
onTap: widget.controller.toggleShowDropdown,
|
||||
child: Container(color: Colors.black.withAlpha(100)),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
47
lib/pangea/common/widgets/tutorial_overlay_message.dart
Normal file
47
lib/pangea/common/widgets/tutorial_overlay_message.dart
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class TutorialOverlayMessage extends StatelessWidget {
|
||||
final String message;
|
||||
|
||||
const TutorialOverlayMessage(
|
||||
this.message, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
width: 200,
|
||||
alignment: Alignment.center,
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
children: [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Icon(
|
||||
Icons.info_outlined,
|
||||
size: 16.0,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
),
|
||||
const WidgetSpan(child: SizedBox(width: 4.0)),
|
||||
TextSpan(
|
||||
text: message,
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue