refactor: position points animation by keys instead of as a positioned widget in a stack (#2230)
This commit is contained in:
parent
027e13f32d
commit
ba7a9ebf53
21 changed files with 565 additions and 603 deletions
|
|
@ -28,6 +28,7 @@ import 'package:fluffychat/pages/chat/event_info_dialog.dart';
|
|||
import 'package:fluffychat/pages/chat/recording_dialog.dart';
|
||||
import 'package:fluffychat/pages/chat_details/chat_details.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/level_up.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/chat/utils/unlocked_morphs_snackbar.dart';
|
||||
|
|
@ -129,8 +130,10 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
// #Pangea
|
||||
final PangeaController pangeaController = MatrixState.pangeaController;
|
||||
late Choreographer choreographer = Choreographer(pangeaController, this);
|
||||
StreamSubscription? _levelSubscription;
|
||||
late GoRouter _router;
|
||||
|
||||
StreamSubscription? _levelSubscription;
|
||||
StreamSubscription? _analyticsSubscription;
|
||||
// Pangea#
|
||||
Room get room => sendingClient.getRoomById(roomId) ?? widget.room;
|
||||
|
||||
|
|
@ -399,6 +402,23 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
}
|
||||
},
|
||||
);
|
||||
|
||||
_analyticsSubscription =
|
||||
pangeaController.getAnalytics.analyticsStream.stream.listen((u) {
|
||||
if (u.targetID == null) return;
|
||||
OverlayUtil.showOverlay(
|
||||
overlayKey: u.targetID,
|
||||
followerAnchor: Alignment.bottomCenter,
|
||||
targetAnchor: Alignment.bottomCenter,
|
||||
context: context,
|
||||
child: PointsGainedAnimation(
|
||||
points: u.points,
|
||||
targetID: u.targetID!,
|
||||
),
|
||||
transformTargetId: u.targetID ?? "",
|
||||
closePrevOverlay: false,
|
||||
);
|
||||
});
|
||||
// Pangea#
|
||||
_tryLoadTimeline();
|
||||
if (kIsWeb) {
|
||||
|
|
@ -645,6 +665,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
stopAudioStream.close();
|
||||
hideTextController.dispose();
|
||||
_levelSubscription?.cancel();
|
||||
_analyticsSubscription?.cancel();
|
||||
_router.routeInformationProvider.removeListener(_onRouteChanged);
|
||||
//Pangea#
|
||||
super.dispose();
|
||||
|
|
@ -798,6 +819,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
pangeaController.putAnalytics.setState(
|
||||
AnalyticsStream(
|
||||
eventId: msgEventId,
|
||||
targetID: msgEventId,
|
||||
roomId: room.id,
|
||||
constructs: [
|
||||
...originalSent.vocabAndMorphUses(
|
||||
|
|
@ -806,7 +828,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
metadata: metadata,
|
||||
),
|
||||
],
|
||||
origin: AnalyticsUpdateOrigin.sendMessage,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,19 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/get_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_style.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class PointsGainedAnimation extends StatefulWidget {
|
||||
final Color? gainColor;
|
||||
final Color? loseColor;
|
||||
final AnalyticsUpdateOrigin origin;
|
||||
final int points;
|
||||
final String targetID;
|
||||
|
||||
const PointsGainedAnimation({
|
||||
super.key,
|
||||
required this.origin,
|
||||
this.gainColor = AppConfig.gold,
|
||||
this.loseColor = Colors.red,
|
||||
required this.points,
|
||||
required this.targetID,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -27,25 +22,22 @@ class PointsGainedAnimation extends StatefulWidget {
|
|||
|
||||
class PointsGainedAnimationState extends State<PointsGainedAnimation>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final Color? gainColor = AppConfig.gold;
|
||||
final Color? loseColor = Colors.red;
|
||||
|
||||
late AnimationController _controller;
|
||||
late Animation<Offset> _offsetAnimation;
|
||||
late Animation<double> _fadeAnimation;
|
||||
final List<Animation<double>> _swayAnimation = [];
|
||||
final List<double> _randomSwayOffset = [];
|
||||
final List<Offset> _particleTrajectories = [];
|
||||
|
||||
StreamSubscription? _pointsSubscription;
|
||||
int? get _prevXP =>
|
||||
MatrixState.pangeaController.getAnalytics.constructListModel.prevXP;
|
||||
int? get _currentXP =>
|
||||
MatrixState.pangeaController.getAnalytics.constructListModel.totalXP;
|
||||
int? _addedPoints;
|
||||
|
||||
final Random _random = Random();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.points == 0) return;
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 2000),
|
||||
vsync: this,
|
||||
|
|
@ -71,14 +63,12 @@ class PointsGainedAnimationState extends State<PointsGainedAnimation>
|
|||
),
|
||||
);
|
||||
|
||||
_pointsSubscription = MatrixState
|
||||
.pangeaController.getAnalytics.analyticsStream.stream
|
||||
.listen(_showPointsGained);
|
||||
_showPointsGained();
|
||||
}
|
||||
|
||||
void initParticleTrajectories() {
|
||||
_particleTrajectories.clear();
|
||||
for (int i = 0; i < (_addedPoints?.abs() ?? 0); i++) {
|
||||
for (int i = 0; i < widget.points.abs(); i++) {
|
||||
final angle = _random.nextDouble() * (pi / 2) +
|
||||
pi / 4; // Random angle in the V-shaped range.
|
||||
const baseSpeed = 20; // Initial base speed.
|
||||
|
|
@ -93,10 +83,9 @@ class PointsGainedAnimationState extends State<PointsGainedAnimation>
|
|||
|
||||
void initSwayAnimations() {
|
||||
_swayAnimation.clear();
|
||||
_randomSwayOffset.clear();
|
||||
initParticleTrajectories();
|
||||
|
||||
for (int i = 0; i < (_addedPoints ?? 0); i++) {
|
||||
for (int i = 0; i < widget.points; i++) {
|
||||
_swayAnimation.add(
|
||||
Tween<double>(
|
||||
begin: 0.0,
|
||||
|
|
@ -108,41 +97,41 @@ class PointsGainedAnimationState extends State<PointsGainedAnimation>
|
|||
),
|
||||
),
|
||||
);
|
||||
_randomSwayOffset.add(_random.nextDouble() * 2 * pi);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_pointsSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _showPointsGained(AnalyticsStreamUpdate update) {
|
||||
if (update.origin != widget.origin) return;
|
||||
setState(() => _addedPoints = (_currentXP ?? 0) - (_prevXP ?? 0));
|
||||
if (_prevXP != _currentXP) {
|
||||
initSwayAnimations();
|
||||
_controller.reset();
|
||||
_controller.forward();
|
||||
}
|
||||
void _showPointsGained() {
|
||||
initSwayAnimations();
|
||||
_controller.reset();
|
||||
_controller.forward().then(
|
||||
(_) {
|
||||
if (!mounted) return;
|
||||
MatrixState.pAnyState.closeOverlay(widget.targetID);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bool get animate =>
|
||||
_currentXP != null &&
|
||||
_prevXP != null &&
|
||||
_addedPoints != null &&
|
||||
_prevXP! != _currentXP!;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!animate) return const SizedBox();
|
||||
if (widget.points == 0) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
MatrixState.pAnyState.closeOverlay(widget.targetID);
|
||||
}
|
||||
});
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
final textColor = _addedPoints! > 0 ? widget.gainColor : widget.loseColor;
|
||||
final textColor = widget.points > 0 ? gainColor : loseColor;
|
||||
|
||||
final plusWidget = Text(
|
||||
_addedPoints! > 0 ? "+" : "-",
|
||||
widget.points > 0 ? "+" : "-",
|
||||
style: BotStyle.text(
|
||||
context,
|
||||
big: true,
|
||||
|
|
@ -153,29 +142,32 @@ class PointsGainedAnimationState extends State<PointsGainedAnimation>
|
|||
),
|
||||
);
|
||||
|
||||
return SlideTransition(
|
||||
position: _offsetAnimation,
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: IgnorePointer(
|
||||
ignoring: _controller.isAnimating,
|
||||
child: Stack(
|
||||
children: List.generate(_addedPoints!.abs(), (index) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
final progress = _controller.value;
|
||||
final trajectory = _particleTrajectories[index];
|
||||
return Transform.translate(
|
||||
offset: Offset(
|
||||
trajectory.dx * pow(progress, 2),
|
||||
trajectory.dy * pow(progress, 2),
|
||||
),
|
||||
child: plusWidget,
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: SlideTransition(
|
||||
position: _offsetAnimation,
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: IgnorePointer(
|
||||
ignoring: _controller.isAnimating,
|
||||
child: Stack(
|
||||
children: List.generate(widget.points.abs(), (index) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
final progress = _controller.value;
|
||||
final trajectory = _particleTrajectories[index];
|
||||
return Transform.translate(
|
||||
offset: Offset(
|
||||
trajectory.dx * pow(progress, 2),
|
||||
trajectory.dy * pow(progress, 2),
|
||||
),
|
||||
child: plusWidget,
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ class GetAnalyticsController extends BaseController {
|
|||
data: {},
|
||||
);
|
||||
} finally {
|
||||
_updateAnalyticsStream();
|
||||
_updateAnalyticsStream(points: 0);
|
||||
if (!initCompleter.isCompleted) initCompleter.complete();
|
||||
_initializing = false;
|
||||
}
|
||||
|
|
@ -154,7 +154,13 @@ class GetAnalyticsController extends BaseController {
|
|||
if (newUnlockedMorphs.isNotEmpty) {
|
||||
_onUnlockMorphLemmas(newUnlockedMorphs);
|
||||
}
|
||||
_updateAnalyticsStream(origin: analyticsUpdate.origin);
|
||||
_updateAnalyticsStream(
|
||||
points: analyticsUpdate.newConstructs.fold<int>(
|
||||
0,
|
||||
(previousValue, element) => previousValue + element.pointValue,
|
||||
),
|
||||
targetID: analyticsUpdate.targetID,
|
||||
);
|
||||
// Update public profile each time that new analytics are added.
|
||||
// If the level hasn't changed, this will not send an update to the server.
|
||||
// Do this on all updates (not just on level updates) to account for cases
|
||||
|
|
@ -165,9 +171,15 @@ class GetAnalyticsController extends BaseController {
|
|||
}
|
||||
|
||||
void _updateAnalyticsStream({
|
||||
AnalyticsUpdateOrigin? origin,
|
||||
required int points,
|
||||
String? targetID,
|
||||
}) =>
|
||||
analyticsStream.add(AnalyticsStreamUpdate(origin: origin));
|
||||
analyticsStream.add(
|
||||
AnalyticsStreamUpdate(
|
||||
points: points,
|
||||
targetID: targetID,
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> _onLevelUp(final int lowerLevel, final int upperLevel) async {
|
||||
final result = await _generateLevelUpAnalyticsAndSaveToStateEvent(
|
||||
|
|
@ -484,9 +496,11 @@ class AnalyticsCacheEntry {
|
|||
}
|
||||
|
||||
class AnalyticsStreamUpdate {
|
||||
final AnalyticsUpdateOrigin? origin;
|
||||
final int points;
|
||||
final String? targetID;
|
||||
|
||||
AnalyticsStreamUpdate({
|
||||
this.origin,
|
||||
required this.points,
|
||||
this.targetID,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
if (roomID != null) _clearDraftUses(roomID);
|
||||
_decideWhetherToUpdateAnalyticsRoom(
|
||||
level,
|
||||
data.origin,
|
||||
data.targetID,
|
||||
data.constructs,
|
||||
);
|
||||
},
|
||||
|
|
@ -164,9 +164,9 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
void addDraftUses(
|
||||
List<PangeaToken> tokens,
|
||||
String roomID,
|
||||
ConstructUseTypeEnum useType,
|
||||
AnalyticsUpdateOrigin origin,
|
||||
) {
|
||||
ConstructUseTypeEnum useType, {
|
||||
String? targetID,
|
||||
}) {
|
||||
final metadata = ConstructUseMetaData(
|
||||
roomId: roomID,
|
||||
timeStamp: DateTime.now(),
|
||||
|
|
@ -230,7 +230,11 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
// so copy it here to that the list of new uses is accurate
|
||||
final List<OneConstructUse> newUses = List.from(uses);
|
||||
_addLocalMessage('draft$roomID', uses).then(
|
||||
(_) => _decideWhetherToUpdateAnalyticsRoom(level, origin, newUses),
|
||||
(_) => _decideWhetherToUpdateAnalyticsRoom(
|
||||
level,
|
||||
targetID,
|
||||
newUses,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -287,7 +291,7 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
/// Otherwise, add a local update to the alert stream.
|
||||
void _decideWhetherToUpdateAnalyticsRoom(
|
||||
int prevLevel,
|
||||
AnalyticsUpdateOrigin? origin,
|
||||
String? targetID,
|
||||
List<OneConstructUse> newConstructs,
|
||||
) {
|
||||
// cancel the last timer that was set on message event and
|
||||
|
|
@ -308,7 +312,7 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
AnalyticsUpdate(
|
||||
AnalyticsUpdateType.local,
|
||||
newConstructs,
|
||||
origin: origin,
|
||||
targetID: targetID,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -426,7 +430,7 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
class AnalyticsStream {
|
||||
final String? eventId;
|
||||
final String? roomId;
|
||||
final AnalyticsUpdateOrigin? origin;
|
||||
final String? targetID;
|
||||
|
||||
final List<OneConstructUse> constructs;
|
||||
|
||||
|
|
@ -434,29 +438,20 @@ class AnalyticsStream {
|
|||
required this.eventId,
|
||||
required this.roomId,
|
||||
required this.constructs,
|
||||
this.origin,
|
||||
this.targetID,
|
||||
});
|
||||
}
|
||||
|
||||
enum AnalyticsUpdateOrigin {
|
||||
it,
|
||||
igc,
|
||||
sendMessage,
|
||||
practiceActivity,
|
||||
inputBar,
|
||||
wordZoom,
|
||||
}
|
||||
|
||||
class AnalyticsUpdate {
|
||||
final AnalyticsUpdateType type;
|
||||
final AnalyticsUpdateOrigin? origin;
|
||||
final List<OneConstructUse> newConstructs;
|
||||
final bool isLogout;
|
||||
final String? targetID;
|
||||
|
||||
AnalyticsUpdate(
|
||||
this.type,
|
||||
this.newConstructs, {
|
||||
this.isLogout = false,
|
||||
this.origin,
|
||||
this.targetID,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/chat_floating_action_button.dart';
|
||||
|
||||
class ChatInputBarHeader extends StatelessWidget {
|
||||
|
|
@ -35,11 +32,6 @@ class ChatInputBarHeader extends StatelessWidget {
|
|||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const PointsGainedAnimation(
|
||||
gainColor: AppConfig.gold,
|
||||
origin: AnalyticsUpdateOrigin.sendMessage,
|
||||
),
|
||||
const SizedBox(width: 100),
|
||||
ChatFloatingActionButton(
|
||||
controller: controller,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import 'package:http/http.dart' as http;
|
|||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart';
|
||||
|
|
@ -333,7 +332,6 @@ class ITController {
|
|||
ignoredTokens ?? [],
|
||||
choreographer.roomId,
|
||||
ConstructUseTypeEnum.ignIt,
|
||||
AnalyticsUpdateOrigin.it,
|
||||
);
|
||||
|
||||
Future.delayed(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../bot/utils/bot_style.dart';
|
||||
import 'it_shimmer.dart';
|
||||
|
||||
|
|
@ -29,7 +30,6 @@ class ChoicesArray extends StatefulWidget {
|
|||
final ChoiceCallback? onLongPress;
|
||||
final int? selectedChoiceIndex;
|
||||
final String originalSpan;
|
||||
final String Function(int) uniqueKeyForLayerLink;
|
||||
|
||||
/// If null then should not be used
|
||||
/// We don't want tts in the case of L1 options
|
||||
|
|
@ -60,7 +60,6 @@ class ChoicesArray extends StatefulWidget {
|
|||
required this.choices,
|
||||
required this.onPressed,
|
||||
required this.originalSpan,
|
||||
required this.uniqueKeyForLayerLink,
|
||||
required this.selectedChoiceIndex,
|
||||
required this.tts,
|
||||
this.enableAudio = true,
|
||||
|
|
@ -214,60 +213,68 @@ class ChoiceItem extends StatelessWidget {
|
|||
waitDuration: onLongPress != null
|
||||
? const Duration(milliseconds: 500)
|
||||
: const Duration(days: 1),
|
||||
child: ChoiceAnimationWidget(
|
||||
key: ValueKey("${entry.value.text}$id"),
|
||||
selected: entry.value.color != null,
|
||||
isGold: entry.value.isGold,
|
||||
enableInteraction: enableInteraction,
|
||||
disableInteraction: disableInteraction,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(2),
|
||||
padding: EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(AppConfig.borderRadius),
|
||||
child: CompositedTransformTarget(
|
||||
link: MatrixState.pAnyState
|
||||
.layerLinkAndKey("${entry.value.text}$id")
|
||||
.link,
|
||||
child: ChoiceAnimationWidget(
|
||||
key: MatrixState.pAnyState
|
||||
.layerLinkAndKey("${entry.value.text}$id")
|
||||
.key,
|
||||
selected: entry.value.color != null,
|
||||
isGold: entry.value.isGold,
|
||||
enableInteraction: enableInteraction,
|
||||
disableInteraction: disableInteraction,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(2),
|
||||
padding: EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? entry.value.color ?? theme.colorScheme.primary
|
||||
: Colors.transparent,
|
||||
style: BorderStyle.solid,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? entry.value.color ?? theme.colorScheme.primary
|
||||
: Colors.transparent,
|
||||
style: BorderStyle.solid,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
),
|
||||
//if index is selected, then give the background a slight primary color
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
entry.value.color?.withAlpha(50) ??
|
||||
theme.colorScheme.primary.withAlpha(10),
|
||||
),
|
||||
textStyle: WidgetStateProperty.all(
|
||||
BotStyle.text(context),
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
),
|
||||
//if index is selected, then give the background a slight primary color
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
entry.value.color?.withAlpha(50) ??
|
||||
theme.colorScheme.primary.withAlpha(10),
|
||||
),
|
||||
textStyle: WidgetStateProperty.all(
|
||||
BotStyle.text(context),
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onLongPress: onLongPress != null && !interactionDisabled
|
||||
? () => onLongPress!(entry.value.text, entry.key)
|
||||
: null,
|
||||
onPressed: interactionDisabled
|
||||
? null
|
||||
: () => onPressed(entry.value.text, entry.key),
|
||||
child: Text(
|
||||
getDisplayCopy != null
|
||||
? getDisplayCopy!(entry.value.text)
|
||||
: entry.value.text,
|
||||
style: BotStyle.text(context).copyWith(
|
||||
fontSize: fontSize,
|
||||
onLongPress: onLongPress != null && !interactionDisabled
|
||||
? () => onLongPress!(entry.value.text, entry.key)
|
||||
: null,
|
||||
onPressed: interactionDisabled
|
||||
? null
|
||||
: () => onPressed(entry.value.text, entry.key),
|
||||
child: Text(
|
||||
getDisplayCopy != null
|
||||
? getDisplayCopy!(entry.value.text)
|
||||
: entry.value.text,
|
||||
style: BotStyle.text(context).copyWith(
|
||||
fontSize: fontSize,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/span_data_type.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/span_data.dart';
|
||||
|
|
@ -150,7 +148,7 @@ class SpanCardState extends State<SpanCard> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> onChoiceSelect(String value, int index) async {
|
||||
Future<void> onChoiceSelect(int index) async {
|
||||
selectedChoiceIndex = index;
|
||||
if (selectedChoice != null) {
|
||||
if (!selectedChoice!.selected) {
|
||||
|
|
@ -160,7 +158,8 @@ class SpanCardState extends State<SpanCard> {
|
|||
selectedChoice!.isBestCorrection
|
||||
? ConstructUseTypeEnum.corIGC
|
||||
: ConstructUseTypeEnum.incIGC,
|
||||
AnalyticsUpdateOrigin.igc,
|
||||
targetID:
|
||||
"${selectedChoice!.value}${widget.scm.pangeaMatch?.hashCode.toString()}",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -191,7 +190,6 @@ class SpanCardState extends State<SpanCard> {
|
|||
ignoredTokens ?? [],
|
||||
widget.roomId,
|
||||
ConstructUseTypeEnum.ignIGC,
|
||||
AnalyticsUpdateOrigin.igc,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -261,156 +259,143 @@ class WordMatchContent extends StatelessWidget {
|
|||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
try {
|
||||
return Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
return Column(
|
||||
children: [
|
||||
const Positioned(
|
||||
top: 40,
|
||||
child: PointsGainedAnimation(
|
||||
origin: AnalyticsUpdateOrigin.igc,
|
||||
// if (!controller.widget.scm.pangeaMatch!.isITStart)
|
||||
CardHeader(
|
||||
text: controller.error?.toString() ?? matchCopy.title,
|
||||
botExpression: controller.error == null
|
||||
? controller.currentExpression
|
||||
: BotExpression.addled,
|
||||
),
|
||||
Scrollbar(
|
||||
controller: scrollController,
|
||||
thumbVisibility: true,
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// const SizedBox(height: 10.0),
|
||||
// if (matchCopy.description != null)
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.only(),
|
||||
// child: Text(
|
||||
// matchCopy.description!,
|
||||
// style: BotStyle.text(context),
|
||||
// ),
|
||||
// ),
|
||||
const SizedBox(height: 8),
|
||||
if (!controller.widget.scm.pangeaMatch!.isITStart)
|
||||
ChoicesArray(
|
||||
originalSpan:
|
||||
controller.widget.scm.pangeaMatch!.matchContent,
|
||||
isLoading: controller.fetchingData,
|
||||
choices: controller.widget.scm.pangeaMatch!.match.choices
|
||||
?.map(
|
||||
(e) => Choice(
|
||||
text: e.value,
|
||||
color: e.selected ? e.type.color : null,
|
||||
isGold: e.type.name == 'bestCorrection',
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onPressed: (value, index) =>
|
||||
controller.onChoiceSelect(index),
|
||||
selectedChoiceIndex: controller.selectedChoiceIndex,
|
||||
tts: controller.tts,
|
||||
id: controller.widget.scm.pangeaMatch!.hashCode
|
||||
.toString(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
PromptAndFeedback(controller: controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// if (!controller.widget.scm.pangeaMatch!.isITStart)
|
||||
CardHeader(
|
||||
text: controller.error?.toString() ?? matchCopy.title,
|
||||
botExpression: controller.error == null
|
||||
? controller.currentExpression
|
||||
: BotExpression.addled,
|
||||
),
|
||||
Scrollbar(
|
||||
controller: scrollController,
|
||||
thumbVisibility: true,
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// const SizedBox(height: 10.0),
|
||||
// if (matchCopy.description != null)
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.only(),
|
||||
// child: Text(
|
||||
// matchCopy.description!,
|
||||
// style: BotStyle.text(context),
|
||||
// ),
|
||||
// ),
|
||||
const SizedBox(height: 8),
|
||||
if (!controller.widget.scm.pangeaMatch!.isITStart)
|
||||
ChoicesArray(
|
||||
originalSpan:
|
||||
controller.widget.scm.pangeaMatch!.matchContent,
|
||||
isLoading: controller.fetchingData,
|
||||
choices:
|
||||
controller.widget.scm.pangeaMatch!.match.choices
|
||||
?.map(
|
||||
(e) => Choice(
|
||||
text: e.value,
|
||||
color: e.selected ? e.type.color : null,
|
||||
isGold: e.type.name == 'bestCorrection',
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onPressed: controller.onChoiceSelect,
|
||||
uniqueKeyForLayerLink: (int index) =>
|
||||
"wordMatch$index",
|
||||
selectedChoiceIndex: controller.selectedChoiceIndex,
|
||||
tts: controller.tts,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
PromptAndFeedback(controller: controller),
|
||||
],
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Opacity(
|
||||
opacity: 0.8,
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
Theme.of(context).colorScheme.primary.withAlpha(25),
|
||||
),
|
||||
),
|
||||
onPressed: controller.onIgnoreMatch,
|
||||
child: Center(
|
||||
child: Text(L10n.of(context).ignoreInThisText),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Opacity(
|
||||
opacity: 0.8,
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
Theme.of(context).colorScheme.primary.withAlpha(25),
|
||||
),
|
||||
),
|
||||
onPressed: controller.onIgnoreMatch,
|
||||
child: Center(
|
||||
child: Text(L10n.of(context).ignoreInThisText),
|
||||
const SizedBox(width: 10),
|
||||
if (!controller.widget.scm.pangeaMatch!.isITStart)
|
||||
Expanded(
|
||||
child: Opacity(
|
||||
opacity: controller.selectedChoiceIndex != null ? 1.0 : 0.5,
|
||||
child: TextButton(
|
||||
onPressed: controller.selectedChoiceIndex != null
|
||||
? controller.onReplaceSelected
|
||||
: null,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
(controller.selectedChoice != null
|
||||
? controller.selectedChoice!.color
|
||||
: Theme.of(context).colorScheme.primary)
|
||||
.withAlpha(50),
|
||||
),
|
||||
// Outline if Replace button enabled
|
||||
side: controller.selectedChoice != null
|
||||
? WidgetStateProperty.all(
|
||||
BorderSide(
|
||||
color: controller.selectedChoice!.color,
|
||||
style: BorderStyle.solid,
|
||||
width: 2.0,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Text(L10n.of(context).replace),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
if (!controller.widget.scm.pangeaMatch!.isITStart)
|
||||
Expanded(
|
||||
child: Opacity(
|
||||
opacity:
|
||||
controller.selectedChoiceIndex != null ? 1.0 : 0.5,
|
||||
child: TextButton(
|
||||
onPressed: controller.selectedChoiceIndex != null
|
||||
? controller.onReplaceSelected
|
||||
: null,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
(controller.selectedChoice != null
|
||||
? controller.selectedChoice!.color
|
||||
: Theme.of(context).colorScheme.primary)
|
||||
.withAlpha(50),
|
||||
),
|
||||
// Outline if Replace button enabled
|
||||
side: controller.selectedChoice != null
|
||||
? WidgetStateProperty.all(
|
||||
BorderSide(
|
||||
color: controller.selectedChoice!.color,
|
||||
style: BorderStyle.solid,
|
||||
width: 2.0,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Text(L10n.of(context).replace),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
if (controller.widget.scm.pangeaMatch!.isITStart)
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
Future.delayed(
|
||||
Duration.zero,
|
||||
() => controller.widget.scm.onITStart(),
|
||||
);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
(Theme.of(context).colorScheme.primary).withAlpha(25),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
if (controller.widget.scm.pangeaMatch!.isITStart)
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
Future.delayed(
|
||||
Duration.zero,
|
||||
() => controller.widget.scm.onITStart(),
|
||||
);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
(Theme.of(context).colorScheme.primary)
|
||||
.withAlpha(25),
|
||||
),
|
||||
),
|
||||
child: Text(L10n.of(context).helpMeTranslate),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// if (controller.widget.scm.pangeaMatch!.isITStart)
|
||||
// DontShowSwitchListTile(
|
||||
// controller: pangeaController,
|
||||
// onSwitch: (bool value) {
|
||||
// pangeaController.userController.updateProfile((profile) {
|
||||
// profile.userSettings.itAutoPlay = value;
|
||||
// return profile;
|
||||
// });
|
||||
// },
|
||||
// ),
|
||||
child: Text(L10n.of(context).helpMeTranslate),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// if (controller.widget.scm.pangeaMatch!.isITStart)
|
||||
// DontShowSwitchListTile(
|
||||
// controller: pangeaController,
|
||||
// onSwitch: (bool value) {
|
||||
// pangeaController.userController.updateProfile((profile) {
|
||||
// profile.userSettings.itAutoPlay = value;
|
||||
// return profile;
|
||||
// });
|
||||
// },
|
||||
// ),
|
||||
],
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/it_controller.dart';
|
||||
|
|
@ -111,142 +109,130 @@ class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
|
|||
: Colors.black,
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(0, 3, 3, 3),
|
||||
child: Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (itController.isEditingSourceText)
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 20,
|
||||
right: 10,
|
||||
top: 10,
|
||||
),
|
||||
child: TextField(
|
||||
controller: TextEditingController(
|
||||
text: itController.sourceText,
|
||||
),
|
||||
autofocus: true,
|
||||
enableSuggestions: false,
|
||||
maxLines: null,
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted:
|
||||
itController.onEditSourceTextSubmit,
|
||||
obscureText: false,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (itController.isEditingSourceText)
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 20,
|
||||
right: 10,
|
||||
top: 10,
|
||||
),
|
||||
if (!itController.isEditingSourceText &&
|
||||
itController.sourceText != null)
|
||||
SizedBox(
|
||||
width: iconDimension,
|
||||
height: iconDimension,
|
||||
child: IconButton(
|
||||
iconSize: iconSize,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
onPressed: () {
|
||||
if (itController.nextITStep != null) {
|
||||
itController.setIsEditingSourceText(true);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
// iconSize: 20,
|
||||
child: TextField(
|
||||
controller: TextEditingController(
|
||||
text: itController.sourceText,
|
||||
),
|
||||
),
|
||||
if (!itController.isEditingSourceText)
|
||||
SizedBox(
|
||||
width: iconDimension,
|
||||
height: iconDimension,
|
||||
child: IconButton(
|
||||
iconSize: iconSize,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (c) => const SettingsLearning(),
|
||||
barrierDismissible: false,
|
||||
),
|
||||
autofocus: true,
|
||||
enableSuggestions: false,
|
||||
maxLines: null,
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted: itController.onEditSourceTextSubmit,
|
||||
obscureText: false,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: iconDimension,
|
||||
height: iconDimension,
|
||||
child: IconButton(
|
||||
iconSize: iconSize,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: () {
|
||||
itController.isEditingSourceText
|
||||
? itController.setIsEditingSourceText(false)
|
||||
: itController.closeIT();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!itController.isEditingSourceText &&
|
||||
itController.sourceText != null)
|
||||
SizedBox(
|
||||
width: iconDimension,
|
||||
height: iconDimension,
|
||||
child: IconButton(
|
||||
iconSize: iconSize,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
onPressed: () {
|
||||
if (itController.nextITStep != null) {
|
||||
itController.setIsEditingSourceText(true);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
// iconSize: 20,
|
||||
),
|
||||
),
|
||||
if (!itController.isEditingSourceText)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: itController.sourceText != null
|
||||
? Text(
|
||||
itController.sourceText!,
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
: const LinearProgressIndicator(),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
if (showITInstructionsTooltip)
|
||||
const InstructionsInlineTooltip(
|
||||
instructionsEnum: InstructionsEnum.clickBestOption,
|
||||
),
|
||||
if (showTranslationsChoicesTooltip)
|
||||
const InstructionsInlineTooltip(
|
||||
instructionsEnum: InstructionsEnum.translationChoices,
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
constraints: const BoxConstraints(minHeight: 80),
|
||||
child: AnimatedSize(
|
||||
duration: itController.animationSpeed,
|
||||
child: Center(
|
||||
child: itController.choreographer.errorService.isError
|
||||
? ITError(
|
||||
error: itController
|
||||
.choreographer.errorService.error!,
|
||||
controller: itController,
|
||||
)
|
||||
: itController.showChoiceFeedback
|
||||
? ChoiceFeedbackText(
|
||||
controller: itController,
|
||||
)
|
||||
: itController.isTranslationDone
|
||||
? TranslationFeedback(
|
||||
controller: itController,
|
||||
)
|
||||
: ITChoices(controller: itController),
|
||||
SizedBox(
|
||||
width: iconDimension,
|
||||
height: iconDimension,
|
||||
child: IconButton(
|
||||
iconSize: iconSize,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (c) => const SettingsLearning(),
|
||||
barrierDismissible: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: iconDimension,
|
||||
height: iconDimension,
|
||||
child: IconButton(
|
||||
iconSize: iconSize,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: () {
|
||||
itController.isEditingSourceText
|
||||
? itController.setIsEditingSourceText(false)
|
||||
: itController.closeIT();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Positioned(
|
||||
top: 60,
|
||||
child: PointsGainedAnimation(
|
||||
origin: AnalyticsUpdateOrigin.it,
|
||||
if (!itController.isEditingSourceText)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: itController.sourceText != null
|
||||
? Text(
|
||||
itController.sourceText!,
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
: const LinearProgressIndicator(),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
if (showITInstructionsTooltip)
|
||||
const InstructionsInlineTooltip(
|
||||
instructionsEnum: InstructionsEnum.clickBestOption,
|
||||
),
|
||||
if (showTranslationsChoicesTooltip)
|
||||
const InstructionsInlineTooltip(
|
||||
instructionsEnum: InstructionsEnum.translationChoices,
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
constraints: const BoxConstraints(minHeight: 80),
|
||||
child: AnimatedSize(
|
||||
duration: itController.animationSpeed,
|
||||
child: Center(
|
||||
child: itController.choreographer.errorService.isError
|
||||
? ITError(
|
||||
error: itController
|
||||
.choreographer.errorService.error!,
|
||||
controller: itController,
|
||||
)
|
||||
: itController.showChoiceFeedback
|
||||
? ChoiceFeedbackText(
|
||||
controller: itController,
|
||||
)
|
||||
: itController.isTranslationDone
|
||||
? TranslationFeedback(
|
||||
controller: itController,
|
||||
)
|
||||
: ITChoices(controller: itController),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -390,7 +376,8 @@ class ITChoices extends StatelessWidget {
|
|||
continuance.level > 1
|
||||
? ConstructUseTypeEnum.incIt
|
||||
: ConstructUseTypeEnum.corIt,
|
||||
AnalyticsUpdateOrigin.it,
|
||||
targetID:
|
||||
"${continuance.text.trim()}${controller.currentITStep.hashCode.toString()}",
|
||||
);
|
||||
}
|
||||
controller.currentITStep!.continuances[index].wasClicked = true;
|
||||
|
|
@ -430,7 +417,6 @@ class ITChoices extends StatelessWidget {
|
|||
}).toList(),
|
||||
onPressed: (value, index) => selectContinuance(index, context),
|
||||
onLongPress: (value, index) => showCard(context, index),
|
||||
uniqueKeyForLayerLink: (int index) => "itChoices$index",
|
||||
selectedChoiceIndex: null,
|
||||
tts: controller.choreographer.tts,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -87,7 +87,6 @@ class AlternativeTranslations extends StatelessWidget {
|
|||
controller.choreographer.altTranslator.translations[index],
|
||||
);
|
||||
},
|
||||
uniqueKeyForLayerLink: (int index) => "altTranslation$index",
|
||||
selectedChoiceIndex: null,
|
||||
tts: null,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ class PangeaAnyState {
|
|||
void openOverlay(
|
||||
OverlayEntry entry,
|
||||
BuildContext context, {
|
||||
bool closePrevOverlay = true,
|
||||
String? overlayKey,
|
||||
}) {
|
||||
if (overlayKey != null &&
|
||||
|
|
@ -59,9 +58,6 @@ class PangeaAnyState {
|
|||
return;
|
||||
}
|
||||
|
||||
if (closePrevOverlay) {
|
||||
closeOverlay();
|
||||
}
|
||||
entries.add(OverlayListEntry(entry, key: overlayKey));
|
||||
Overlay.of(context).insert(entry);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,6 @@ class OverlayUtil {
|
|||
MatrixState.pAnyState.openOverlay(
|
||||
entry,
|
||||
context,
|
||||
closePrevOverlay: closePrevOverlay,
|
||||
overlayKey: overlayKey,
|
||||
);
|
||||
} catch (err, stack) {
|
||||
|
|
|
|||
|
|
@ -69,12 +69,15 @@ class MessageMatchActivity extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (overlayController.messageAnalyticsEntry == null ||
|
||||
overlayController.messageLemmaInfos == null ||
|
||||
activityType == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
if (overlayController.messageLemmaInfos == null) {
|
||||
return const CircularProgressIndicator.adaptive();
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
|
|
|
|||
|
|
@ -142,7 +142,10 @@ class MessageMatchActivityItemState extends State<MessageMatchActivityItem> {
|
|||
Widget build(BuildContext context) {
|
||||
return LongPressDraggable<ConstructForm>(
|
||||
data: widget.constructForm,
|
||||
feedback: content(context),
|
||||
feedback: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: content(context),
|
||||
),
|
||||
delay: const Duration(milliseconds: 100),
|
||||
onDragStarted: () {
|
||||
widget.overlayController.onChoiceSelect(widget.constructForm, true);
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ class MessageMorphInputBarContentState
|
|||
form: token!.text.content,
|
||||
),
|
||||
],
|
||||
origin: AnalyticsUpdateOrigin.wordZoom,
|
||||
targetID: token!.text.uniqueKey,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -183,14 +183,17 @@ class MessageMorphInputBarContentState
|
|||
size: const Size(30, 30),
|
||||
showTooltip: false,
|
||||
),
|
||||
Text(
|
||||
L10n.of(context).whatIsTheMorphTag(
|
||||
morph!.getDisplayCopy(context),
|
||||
token!.text.content,
|
||||
Flexible(
|
||||
child: Text(
|
||||
L10n.of(context).whatIsTheMorphTag(
|
||||
morph!.getDisplayCopy(context),
|
||||
token!.text.content,
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
|
|
@ -53,7 +52,6 @@ class ReadingAssistanceInputBar extends StatelessWidget {
|
|||
),
|
||||
overlayController: overlayController,
|
||||
morphFeature: morphFeature,
|
||||
location: AnalyticsUpdateOrigin.inputBar,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -167,6 +167,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
} finally {
|
||||
_initializeSelectedToken();
|
||||
_setInitialToolbarMode();
|
||||
messageLemmaInfos ??= {};
|
||||
initialized = true;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
|
@ -290,7 +291,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
pangeaMessageEvent: pangeaMessageEvent!,
|
||||
overlayController: this,
|
||||
);
|
||||
if (context.mounted) {
|
||||
if (mounted) {
|
||||
OverlayUtil.showPositionedCard(
|
||||
context: context,
|
||||
cardToShow: entry,
|
||||
|
|
@ -363,7 +364,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
form: token.text.content,
|
||||
),
|
||||
],
|
||||
origin: AnalyticsUpdateOrigin.wordZoom,
|
||||
targetID: token.text.uniqueKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,7 +115,6 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
widget.practiceCardController.currentActivity!,
|
||||
widget.practiceCardController.metadata,
|
||||
),
|
||||
origin: AnalyticsUpdateOrigin.practiceActivity,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -226,7 +225,6 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
),
|
||||
ChoicesArray(
|
||||
isLoading: false,
|
||||
uniqueKeyForLayerLink: (index) => "multiple_choice_$index",
|
||||
originalSpan: "placeholder",
|
||||
onPressed: updateChoice,
|
||||
selectedChoiceIndex: selectedChoiceIndex,
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import 'package:collection/collection.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';
|
||||
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/card_error_widget.dart';
|
||||
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
|
|
@ -36,7 +34,6 @@ class PracticeActivityCard extends StatefulWidget {
|
|||
final TargetTokensAndActivityType targetTokensAndActivityType;
|
||||
final MessageOverlayController overlayController;
|
||||
final WordZoomWidget? wordDetailsController;
|
||||
final AnalyticsUpdateOrigin location;
|
||||
|
||||
final String? morphFeature;
|
||||
|
||||
|
|
@ -47,7 +44,6 @@ class PracticeActivityCard extends StatefulWidget {
|
|||
required this.overlayController,
|
||||
this.morphFeature,
|
||||
this.wordDetailsController,
|
||||
required this.location,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -341,12 +337,6 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Main content
|
||||
Positioned(
|
||||
child: PointsGainedAnimation(
|
||||
origin: widget.location,
|
||||
),
|
||||
),
|
||||
if (activityWidget != null) activityWidget!,
|
||||
// Conditionally show the darkening and progress indicator based on the loading state
|
||||
if (!savoringTheJoy && fetchingActivity) ...[
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import 'package:matrix/matrix_api_lite/model/message_types.dart';
|
|||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
|
||||
|
|
@ -58,7 +57,6 @@ class ReadingAssistanceContentState extends State<ReadingAssistanceContent> {
|
|||
targetTokensAndActivityType: widget
|
||||
.overlayController.messageAnalyticsEntry!
|
||||
.nextActivity(ActivityTypeEnum.hiddenWordListening)!,
|
||||
location: AnalyticsUpdateOrigin.practiceActivity,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +69,6 @@ class ReadingAssistanceContentState extends State<ReadingAssistanceContent> {
|
|||
targetTokensAndActivityType: widget
|
||||
.overlayController.messageAnalyticsEntry!
|
||||
.nextActivity(ActivityTypeEnum.messageMeaning)!,
|
||||
location: AnalyticsUpdateOrigin.practiceActivity,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
|
||||
|
|
@ -54,136 +52,126 @@ class WordZoomWidget extends StatelessWidget {
|
|||
maxHeight: AppConfig.toolbarMaxHeight,
|
||||
maxWidth: AppConfig.toolbarMinWidth,
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
const Positioned(
|
||||
child: PointsGainedAnimation(
|
||||
origin: AnalyticsUpdateOrigin.wordZoom,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 40,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
//@ggurdin - might need to play with size to properly center
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
overlayController.onClickOverlayMessageToken(token),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
LemmaWidget(
|
||||
token: _selectedToken,
|
||||
pangeaMessageEvent: messageEvent,
|
||||
// onEdit: () => _setHideCenterContent(true),
|
||||
onEdit: () {
|
||||
debugPrint("what are we doing edits with?");
|
||||
},
|
||||
onEditDone: () {
|
||||
debugPrint("what are we doing edits with?");
|
||||
onEditDone();
|
||||
},
|
||||
tts: tts,
|
||||
overlayController: overlayController,
|
||||
),
|
||||
ConstructXpWidget(
|
||||
id: token.vocabConstructID,
|
||||
onTap: () => showDialog<AnalyticsPopupWrapper>(
|
||||
context: context,
|
||||
builder: (context) => AnalyticsPopupWrapper(
|
||||
constructZoom: token.vocabConstructID,
|
||||
view: ConstructTypeEnum.vocab,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 40,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
//@ggurdin - might need to play with size to properly center
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
overlayController.onClickOverlayMessageToken(token),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
LemmaWidget(
|
||||
token: _selectedToken,
|
||||
pangeaMessageEvent: messageEvent,
|
||||
// onEdit: () => _setHideCenterContent(true),
|
||||
onEdit: () {
|
||||
debugPrint("what are we doing edits with?");
|
||||
},
|
||||
onEditDone: () {
|
||||
debugPrint("what are we doing edits with?");
|
||||
onEditDone();
|
||||
},
|
||||
tts: tts,
|
||||
overlayController: overlayController,
|
||||
),
|
||||
ConstructXpWidget(
|
||||
id: token.vocabConstructID,
|
||||
onTap: () => showDialog<AnalyticsPopupWrapper>(
|
||||
context: context,
|
||||
builder: (context) => AnalyticsPopupWrapper(
|
||||
constructZoom: token.vocabConstructID,
|
||||
view: ConstructTypeEnum.vocab,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 40,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: LemmaEmojiRow(
|
||||
cId: _selectedToken.vocabConstructID,
|
||||
onTapOverride: hasEmojiActivity
|
||||
? () => overlayController.updateToolbarMode(
|
||||
MessageMode.wordEmoji,
|
||||
)
|
||||
: null,
|
||||
isSelected: overlayController.toolbarMode ==
|
||||
MessageMode.wordEmoji,
|
||||
emojiSetCallback: () =>
|
||||
overlayController.setState(() {}),
|
||||
shouldShowEmojis: !hasEmojiActivity,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Container(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 40,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 8,
|
||||
children: [
|
||||
LemmaMeaningWidget(
|
||||
constructUse: token.vocabConstructID.constructUses,
|
||||
langCode: MatrixState.pangeaController
|
||||
.languageController.userL2?.langCodeShort ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
token: overlayController.selectedToken!,
|
||||
controller: overlayController,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
child: LemmaEmojiRow(
|
||||
cId: _selectedToken.vocabConstructID,
|
||||
onTapOverride: hasEmojiActivity
|
||||
? () => overlayController.updateToolbarMode(
|
||||
MessageMode.wordEmoji,
|
||||
)
|
||||
: null,
|
||||
isSelected:
|
||||
overlayController.toolbarMode == MessageMode.wordEmoji,
|
||||
emojiSetCallback: () => overlayController.setState(() {}),
|
||||
shouldShowEmojis: !hasEmojiActivity,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
if (!_selectedToken.doesLemmaTextMatchTokenText) ...[
|
||||
Text(
|
||||
_selectedToken.text.content,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
WordAudioButton(
|
||||
text: _selectedToken.text.content,
|
||||
isSelected: MessageMode.listening ==
|
||||
overlayController.toolbarMode,
|
||||
baseOpacity: 0.4,
|
||||
callbackOverride: overlayController
|
||||
.messageAnalyticsEntry
|
||||
?.hasActivity(
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Container(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 40,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 8,
|
||||
children: [
|
||||
LemmaMeaningWidget(
|
||||
constructUse: token.vocabConstructID.constructUses,
|
||||
langCode: MatrixState.pangeaController.languageController
|
||||
.userL2?.langCodeShort ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
token: overlayController.selectedToken!,
|
||||
controller: overlayController,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
if (!_selectedToken.doesLemmaTextMatchTokenText) ...[
|
||||
Text(
|
||||
_selectedToken.text.content,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
WordAudioButton(
|
||||
text: _selectedToken.text.content,
|
||||
isSelected:
|
||||
MessageMode.listening == overlayController.toolbarMode,
|
||||
baseOpacity: 0.4,
|
||||
callbackOverride:
|
||||
overlayController.messageAnalyticsEntry?.hasActivity(
|
||||
MessageMode.listening.associatedActivityType!,
|
||||
_selectedToken,
|
||||
) ==
|
||||
|
|
@ -191,33 +179,30 @@ class WordZoomWidget extends StatelessWidget {
|
|||
? () => overlayController
|
||||
.updateToolbarMode(MessageMode.listening)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
..._selectedToken
|
||||
.morphsBasicallyEligibleForPracticeByPriority
|
||||
.map(
|
||||
(cId) => MorphologicalListItem(
|
||||
morphFeature: MorphFeaturesEnumExtension.fromString(
|
||||
cId.category,
|
||||
),
|
||||
token: _selectedToken,
|
||||
overlayController: overlayController,
|
||||
),
|
||||
),
|
||||
],
|
||||
..._selectedToken.morphsBasicallyEligibleForPracticeByPriority
|
||||
.map(
|
||||
(cId) => MorphologicalListItem(
|
||||
morphFeature: MorphFeaturesEnumExtension.fromString(
|
||||
cId.category,
|
||||
),
|
||||
],
|
||||
token: _selectedToken,
|
||||
overlayController: overlayController,
|
||||
),
|
||||
),
|
||||
// if (_selectedMorphFeature != null)
|
||||
// MorphologicalCenterWidget(
|
||||
// token: token,
|
||||
// morphFeature: _selectedMorphFeature!,
|
||||
// pangeaMessageEvent: messageEvent,
|
||||
// overlayController: overlayController,
|
||||
// onEditDone: onEditDone,
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
// if (_selectedMorphFeature != null)
|
||||
// MorphologicalCenterWidget(
|
||||
// token: token,
|
||||
// morphFeature: _selectedMorphFeature!,
|
||||
// pangeaMessageEvent: messageEvent,
|
||||
// overlayController: overlayController,
|
||||
// onEditDone: onEditDone,
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue