widgets refactor
This commit is contained in:
parent
9ce99daac9
commit
2b522b6dd7
40 changed files with 873 additions and 1669 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",
|
||||
"failedToLoadFeedback": "Failed to load feedback."
|
||||
}
|
||||
|
|
@ -14,12 +14,10 @@ import 'package:fluffychat/pages/chat/chat_app_bar_title.dart';
|
|||
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_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';
|
||||
import 'package:fluffychat/pangea/chat/widgets/chat_floating_action_button.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/chat_input_bar.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/chat_input_bar_header.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/chat_view_background.dart';
|
||||
import 'package:fluffychat/utils/account_config.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
|
|
@ -206,9 +204,7 @@ class ChatView extends StatelessWidget {
|
|||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// #Pangea
|
||||
// actionsIconTheme:
|
||||
// IconThemeData(
|
||||
// #Pangea
|
||||
// actionsIconTheme: IconThemeData(
|
||||
// color: controller.selectedEvents.isEmpty
|
||||
// ? null
|
||||
// : theme.colorScheme.onTertiaryContainer,
|
||||
|
|
@ -244,11 +240,15 @@ class ChatView extends StatelessWidget {
|
|||
),
|
||||
// #Pangea
|
||||
// builder: (context, _) => UnreadRoomsBadge(
|
||||
// filter: (r) => r.id != controller.roomId,
|
||||
// badgePosition:
|
||||
// BadgePosition.topEnd(end: 8, top: 4),
|
||||
// child: const Center(child: BackButton()),
|
||||
// ),
|
||||
builder: (context, _) => Center(
|
||||
child: SizedBox(
|
||||
height: kToolbarHeight,
|
||||
child: UnreadRoomsBadge(
|
||||
// Pangea#
|
||||
filter: (r) => r.id != controller.roomId,
|
||||
badgePosition: BadgePosition.topEnd(
|
||||
end: 8,
|
||||
|
|
@ -258,6 +258,7 @@ class ChatView extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
// Pangea#
|
||||
),
|
||||
titleSpacing: FluffyThemes.isColumnMode(context) ? 24 : 0,
|
||||
title: ChatAppBarTitle(controller),
|
||||
|
|
@ -267,13 +268,6 @@ class ChatView extends StatelessWidget {
|
|||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// #Pangea
|
||||
if (!controller.showActivityDropdown)
|
||||
Divider(
|
||||
height: 1,
|
||||
color: theme.dividerColor,
|
||||
),
|
||||
// Pangea#
|
||||
PinnedEvents(controller),
|
||||
if (scrollUpBannerEventId != null)
|
||||
ChatAppBarListTile(
|
||||
|
|
@ -318,13 +312,16 @@ class ChatView extends StatelessWidget {
|
|||
// ),
|
||||
// )
|
||||
// : null,
|
||||
floatingActionButton: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 56.0),
|
||||
child: ChatFloatingActionButton(controller: controller),
|
||||
),
|
||||
// body: DropTarget(
|
||||
// onDragDone: controller.onDragDone,
|
||||
// onDragEntered: controller.onDragEntered,
|
||||
// onDragExited: controller.onDragExited,
|
||||
// child: Stack(
|
||||
body: Stack(
|
||||
// Pangea#
|
||||
children: <Widget>[
|
||||
if (accountConfig.wallpaperUrl != null)
|
||||
Opacity(
|
||||
|
|
@ -338,106 +335,116 @@ class ChatView extends StatelessWidget {
|
|||
cacheKey: accountConfig.wallpaperUrl.toString(),
|
||||
uri: accountConfig.wallpaperUrl,
|
||||
fit: BoxFit.cover,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: MediaQuery.sizeOf(context).height,
|
||||
width: MediaQuery.sizeOf(context).width,
|
||||
isThumbnail: false,
|
||||
placeholder: (_) => Container(),
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
// #Pangea
|
||||
// child: Column(
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
// Pangea#
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: controller.clearSingleSelectedEvent,
|
||||
child: ChatEventList(controller: controller),
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: controller.clearSingleSelectedEvent,
|
||||
// #Pangea
|
||||
// child: ChatEventList(controller: controller),
|
||||
child: Stack(
|
||||
children: [
|
||||
ChatEventList(controller: controller),
|
||||
ChatViewBackground(controller.choreographer),
|
||||
],
|
||||
),
|
||||
// #Pangea
|
||||
// if (controller.showScrollDownButton)
|
||||
// Divider(
|
||||
// height: 1,
|
||||
// color: theme.dividerColor,
|
||||
// ),
|
||||
// Pangea#
|
||||
if (controller.room.isExtinct)
|
||||
Container(
|
||||
margin: EdgeInsets.all(bottomSheetPadding),
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
label: Text(L10n.of(context).enterNewChat),
|
||||
onPressed: controller.goToNewRoomAction,
|
||||
),
|
||||
)
|
||||
// #Pangea
|
||||
// else if (controller.room.canSendDefaultMessages &&
|
||||
// controller.room.membership == Membership.join)
|
||||
else if (controller.room.canSendDefaultMessages &&
|
||||
controller.room.membership == Membership.join &&
|
||||
controller.room.isAbandonedDMRoom == true)
|
||||
// Pangea#
|
||||
Container(
|
||||
margin: EdgeInsets.all(bottomSheetPadding),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: FluffyThemes.maxTimelineWidth,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Material(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
color: controller.selectedEvents.isNotEmpty
|
||||
? theme.colorScheme.tertiaryContainer
|
||||
: theme.colorScheme.surfaceContainerHigh,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(24),
|
||||
),
|
||||
child: controller.room.isAbandonedDMRoom ==
|
||||
true
|
||||
? Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.all(
|
||||
16,
|
||||
),
|
||||
foregroundColor:
|
||||
theme.colorScheme.error,
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.archive_outlined,
|
||||
),
|
||||
onPressed: controller.leaveChat,
|
||||
label: Text(
|
||||
L10n.of(context).leave,
|
||||
),
|
||||
),
|
||||
),
|
||||
// #Pangea
|
||||
// if (controller.showScrollDownButton)
|
||||
// Divider(
|
||||
// height: 1,
|
||||
// color: theme.dividerColor,
|
||||
// ),
|
||||
ListenableBuilder(
|
||||
listenable: controller.scrollController,
|
||||
builder: (context, _) {
|
||||
if (controller.scrollController.hasClients &&
|
||||
controller.scrollController.position.pixels >
|
||||
0) {
|
||||
return Divider(
|
||||
height: 1,
|
||||
color: theme.dividerColor,
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
// Pangea#
|
||||
if (controller.room.isExtinct)
|
||||
Container(
|
||||
margin: EdgeInsets.all(bottomSheetPadding),
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
label: Text(L10n.of(context).enterNewChat),
|
||||
onPressed: controller.goToNewRoomAction,
|
||||
),
|
||||
)
|
||||
else if (controller.room.canSendDefaultMessages &&
|
||||
controller.room.membership == Membership.join)
|
||||
Container(
|
||||
margin: EdgeInsets.all(bottomSheetPadding),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: FluffyThemes.maxTimelineWidth,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Material(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
color: controller.selectedEvents.isNotEmpty
|
||||
? theme.colorScheme.tertiaryContainer
|
||||
: theme.colorScheme.surfaceContainerHigh,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(24),
|
||||
),
|
||||
child: controller.room.isAbandonedDMRoom == true
|
||||
? Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.all(
|
||||
16,
|
||||
),
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.all(
|
||||
16,
|
||||
),
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.forum_outlined,
|
||||
),
|
||||
onPressed:
|
||||
controller.recreateChat,
|
||||
label: Text(
|
||||
L10n.of(context).reopenChat,
|
||||
),
|
||||
foregroundColor:
|
||||
theme.colorScheme.error,
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.archive_outlined,
|
||||
),
|
||||
onPressed: controller.leaveChat,
|
||||
label: Text(
|
||||
L10n.of(context).leave,
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.all(
|
||||
16,
|
||||
),
|
||||
],
|
||||
)
|
||||
// #Pangea
|
||||
: null,
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.forum_outlined,
|
||||
),
|
||||
onPressed: controller.recreateChat,
|
||||
label: Text(
|
||||
L10n.of(context).reopenChat,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
// #Pangea
|
||||
// : Column(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
|
|
@ -446,85 +453,25 @@ class ChatView extends StatelessWidget {
|
|||
// ChatEmojiPicker(controller),
|
||||
// ],
|
||||
// ),
|
||||
// Pangea#
|
||||
),
|
||||
),
|
||||
// #Pangea
|
||||
// Keep messages above minimum input bar height
|
||||
if (!controller.room.isAbandonedDMRoom &&
|
||||
controller.room.canSendDefaultMessages &&
|
||||
controller.room.membership == Membership.join &&
|
||||
(controller.room.activityPlan == null ||
|
||||
!controller.room.showActivityChatUI ||
|
||||
controller.room.isActiveInActivity))
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: SizedBox(
|
||||
height: controller.inputBarHeight,
|
||||
),
|
||||
),
|
||||
if (controller.room.isActivityFinished)
|
||||
LoadActivitySummaryWidget(
|
||||
room: controller.room,
|
||||
),
|
||||
: ChatInputBar(
|
||||
controller: controller,
|
||||
padding: bottomSheetPadding,
|
||||
),
|
||||
|
||||
ActivityFinishedStatusMessage(
|
||||
controller: controller,
|
||||
),
|
||||
// Pangea#
|
||||
],
|
||||
),
|
||||
// #Pangea
|
||||
ChatViewBackground(controller.choreographer),
|
||||
if (!controller.room.isAbandonedDMRoom &&
|
||||
controller.room.canSendDefaultMessages &&
|
||||
controller.room.membership == Membership.join &&
|
||||
(controller.room.activityPlan == null ||
|
||||
!controller.room.showActivityChatUI ||
|
||||
controller.room.isActiveInActivity))
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ChatInputBarHeader(
|
||||
controller: controller,
|
||||
padding: bottomSheetPadding,
|
||||
),
|
||||
if (controller.showScrollDownButton)
|
||||
Divider(
|
||||
height: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: ChatInputBar(
|
||||
controller: controller,
|
||||
padding: bottomSheetPadding,
|
||||
),
|
||||
),
|
||||
],
|
||||
// Pangea#
|
||||
),
|
||||
),
|
||||
ActivityStatsMenu(controller),
|
||||
if (controller.room.activitySummary?.summary != null &&
|
||||
controller.hasRainedConfetti == false)
|
||||
StarRainWidget(
|
||||
showBlast: true,
|
||||
onFinished: () =>
|
||||
controller.setHasRainedConfetti(true),
|
||||
),
|
||||
// Pangea#
|
||||
],
|
||||
),
|
||||
),
|
||||
// #Pangea
|
||||
ActivityStatsMenu(controller),
|
||||
if (controller.room.activitySummary?.summary != null &&
|
||||
controller.hasRainedConfetti == false)
|
||||
StarRainWidget(
|
||||
showBlast: true,
|
||||
onFinished: () => controller.setHasRainedConfetti(true),
|
||||
),
|
||||
// if (controller.dragging)
|
||||
// Container(
|
||||
// color: theme.scaffoldBackgroundColor.withAlpha(230),
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -4,7 +4,6 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/extensions/choreographer_state_extension.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
|
||||
|
||||
class ActivityRoleTooltip extends StatelessWidget {
|
||||
|
|
@ -24,7 +23,7 @@ class ActivityRoleTooltip extends StatelessWidget {
|
|||
builder: (context, _) {
|
||||
if (!room.showActivityChatUI ||
|
||||
room.ownRole?.goal == null ||
|
||||
choreographer.isITOpen) {
|
||||
choreographer.itController.open.value) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,91 +1,47 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/extensions/choreographer_state_extension.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/has_error_button.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/language_permissions_warning_buttons.dart';
|
||||
import 'package:fluffychat/pangea/spaces/models/space_model.dart';
|
||||
|
||||
class ChatFloatingActionButton extends StatefulWidget {
|
||||
class ChatFloatingActionButton extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
const ChatFloatingActionButton({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
ChatFloatingActionButtonState createState() =>
|
||||
ChatFloatingActionButtonState();
|
||||
}
|
||||
|
||||
class ChatFloatingActionButtonState extends State<ChatFloatingActionButton> {
|
||||
bool showPermissionsError = false;
|
||||
StreamSubscription? _choreoSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final permissionsController =
|
||||
widget.controller.pangeaController.permissionsController;
|
||||
final itEnabled = permissionsController.isToolEnabled(
|
||||
ToolSetting.interactiveTranslator,
|
||||
widget.controller.room,
|
||||
);
|
||||
final igcEnabled = permissionsController.isToolEnabled(
|
||||
ToolSetting.interactiveGrammar,
|
||||
widget.controller.room,
|
||||
);
|
||||
showPermissionsError = !itEnabled || !igcEnabled;
|
||||
|
||||
if (showPermissionsError) {
|
||||
Future.delayed(
|
||||
const Duration(seconds: 5),
|
||||
() {
|
||||
if (mounted) setState(() => showPermissionsError = false);
|
||||
},
|
||||
);
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_choreoSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.controller.selectedEvents.isNotEmpty) {
|
||||
if (controller.selectedEvents.isNotEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (widget.controller.showScrollDownButton) {
|
||||
return FloatingActionButton(
|
||||
onPressed: widget.controller.scrollDown,
|
||||
heroTag: null,
|
||||
mini: true,
|
||||
child: const Icon(Icons.arrow_downward_outlined),
|
||||
);
|
||||
}
|
||||
|
||||
return ListenableBuilder(
|
||||
listenable: widget.controller.choreographer,
|
||||
listenable: Listenable.merge(
|
||||
[
|
||||
controller.choreographer,
|
||||
controller.scrollController,
|
||||
],
|
||||
),
|
||||
builder: (context, _) {
|
||||
if (widget.controller.choreographer.errorService.error != null &&
|
||||
!widget.controller.choreographer.isITOpen) {
|
||||
return ChoreographerHasErrorButton(
|
||||
widget.controller.choreographer.errorService.error!,
|
||||
widget.controller.choreographer,
|
||||
if (controller.scrollController.hasClients &&
|
||||
controller.scrollController.position.pixels > 0) {
|
||||
return FloatingActionButton(
|
||||
onPressed: controller.scrollDown,
|
||||
heroTag: null,
|
||||
mini: true,
|
||||
child: const Icon(Icons.arrow_downward_outlined),
|
||||
);
|
||||
}
|
||||
|
||||
return showPermissionsError
|
||||
? LanguagePermissionsButtons(
|
||||
choreographer: widget.controller.choreographer,
|
||||
roomID: widget.controller.roomId,
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
if (controller.choreographer.errorService.error != null &&
|
||||
!controller.choreographer.itController.open.value) {
|
||||
return ChoreographerHasErrorButton(
|
||||
controller.choreographer.errorService.error!,
|
||||
controller.choreographer,
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_emoji_picker.dart';
|
||||
import 'package:fluffychat/pages/chat/reply_display.dart';
|
||||
|
|
@ -10,7 +7,7 @@ import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activi
|
|||
import 'package:fluffychat/pangea/chat/widgets/pangea_chat_input_row.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart';
|
||||
|
||||
class ChatInputBar extends StatefulWidget {
|
||||
class ChatInputBar extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
final double padding;
|
||||
|
||||
|
|
@ -20,92 +17,21 @@ class ChatInputBar extends StatefulWidget {
|
|||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChatInputBar> createState() => ChatInputBarState();
|
||||
}
|
||||
|
||||
class ChatInputBarState extends State<ChatInputBar> {
|
||||
Timer? _debounceTimer;
|
||||
|
||||
void _updateHeight() {
|
||||
_debounceTimer = Timer(const Duration(milliseconds: 100), () {
|
||||
final renderBox = context.findRenderObject() as RenderBox?;
|
||||
if (renderBox == null || !renderBox.hasSize) return;
|
||||
widget.controller.updateInputBarHeight(renderBox.size.height);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _updateHeight());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debounceTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return NotificationListener(
|
||||
onNotification: (SizeChangedLayoutNotification notification) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _updateHeight());
|
||||
return true;
|
||||
},
|
||||
child: SizeChangedLayoutNotifier(
|
||||
child: Column(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: FluffyThemes.maxTimelineWidth,
|
||||
),
|
||||
child: ActivityRoleTooltip(
|
||||
choreographer: widget.controller.choreographer,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.all(
|
||||
widget.padding,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: FluffyThemes.maxTimelineWidth,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Material(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
type: MaterialType.transparency,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(24),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ITBar(choreographer: widget.controller.choreographer),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHighest,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
if (!widget.controller.obscureText)
|
||||
ReplyDisplay(widget.controller),
|
||||
PangeaChatInputRow(
|
||||
controller: widget.controller,
|
||||
),
|
||||
ChatEmojiPicker(widget.controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ActivityRoleTooltip(
|
||||
choreographer: controller.choreographer,
|
||||
),
|
||||
),
|
||||
ITBar(choreographer: controller.choreographer),
|
||||
if (!controller.obscureText) ReplyDisplay(controller),
|
||||
PangeaChatInputRow(
|
||||
controller: controller,
|
||||
),
|
||||
ChatEmojiPicker(controller),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import 'dart:ui';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/extensions/choreographer_state_extension.dart';
|
||||
|
||||
class ChatViewBackground extends StatelessWidget {
|
||||
final Choreographer choreographer;
|
||||
|
|
@ -14,7 +13,7 @@ class ChatViewBackground extends StatelessWidget {
|
|||
return ListenableBuilder(
|
||||
listenable: choreographer,
|
||||
builder: (context, _) {
|
||||
return choreographer.isITOpen
|
||||
return choreographer.itController.open.value
|
||||
? Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import 'package:fluffychat/config/themes.dart';
|
|||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/input_bar.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/extensions/choreographer_state_extension.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/extensions/choreographer_ui_extension.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart';
|
||||
|
|
@ -29,7 +28,7 @@ class PangeaChatInputRow extends StatelessWidget {
|
|||
controller.pangeaController.languageController.activeL2Model();
|
||||
|
||||
String hintText(BuildContext context) {
|
||||
if (controller.choreographer.isITOpen) {
|
||||
if (controller.choreographer.itController.open.value) {
|
||||
return L10n.of(context).buildTranslation;
|
||||
}
|
||||
return activel1 != null &&
|
||||
|
|
@ -227,7 +226,7 @@ class PangeaChatInputRow extends StatelessWidget {
|
|||
alignment: Alignment.center,
|
||||
child: PlatformInfos.platformCanRecord &&
|
||||
controller.sendController.text.isEmpty &&
|
||||
!controller.choreographer.isITOpen
|
||||
!controller.choreographer.itController.open.value
|
||||
? FloatingActionButton.small(
|
||||
tooltip: L10n.of(context).voiceMessage,
|
||||
onPressed: controller.voiceMessageAction,
|
||||
|
|
|
|||
|
|
@ -2,19 +2,17 @@ import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
|||
import 'package:fluffychat/pangea/choreographer/controllers/extensions/choregrapher_user_settings_extension.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/assistance_state_enum.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/choreo_mode.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/it_step.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_state.dart';
|
||||
|
||||
extension ChoregrapherUserSettingsExtension on Choreographer {
|
||||
bool get isITOpen => itController.open;
|
||||
bool get isEditingSourceText => itController.editing;
|
||||
bool get isITDone => itController.isTranslationDone;
|
||||
bool get isRunningIT => choreoMode == ChoreoMode.it && !isITDone;
|
||||
List<Continuance>? get itStepContinuances => itController.continuances;
|
||||
bool get isRunningIT {
|
||||
return choreoMode == ChoreoMode.it &&
|
||||
itController.currentITStep.value?.isFinal != true;
|
||||
}
|
||||
|
||||
String? get currentIGCText => igc.currentText;
|
||||
PangeaMatchState? get openIGCMatch => igc.openMatch;
|
||||
PangeaMatchState? get firstIGCMatch => igc.firstOpenMatch;
|
||||
PangeaMatchState? get openMatch => igc.openMatch;
|
||||
PangeaMatchState? get firstOpenMatch => igc.firstOpenMatch;
|
||||
List<PangeaMatchState>? get openIGCMatches => igc.openMatches;
|
||||
List<PangeaMatchState>? get closedIGCMatches => igc.closedMatches;
|
||||
bool get canShowFirstIGCMatch => igc.canShowFirstMatch;
|
||||
|
|
@ -23,15 +21,19 @@ extension ChoregrapherUserSettingsExtension on Choreographer {
|
|||
AssistanceState get assistanceState {
|
||||
final isSubscribed = pangeaController.subscriptionController.isSubscribed;
|
||||
if (isSubscribed == false) return AssistanceState.noSub;
|
||||
if (currentText.isEmpty && sourceText == null) {
|
||||
if (currentText.isEmpty && sourceText.value == null) {
|
||||
return AssistanceState.noMessage;
|
||||
}
|
||||
|
||||
if (errorService.isError) {
|
||||
return AssistanceState.error;
|
||||
}
|
||||
|
||||
if (igc.hasOpenMatches || isRunningIT) {
|
||||
return AssistanceState.fetched;
|
||||
}
|
||||
|
||||
if (isFetching) return AssistanceState.fetching;
|
||||
if (isFetching.value) return AssistanceState.fetching;
|
||||
if (!igc.hasIGCTextData) return AssistanceState.notFetched;
|
||||
return AssistanceState.complete;
|
||||
}
|
||||
|
|
@ -52,7 +54,7 @@ extension ChoregrapherUserSettingsExtension on Choreographer {
|
|||
if (!isAutoIGCEnabled) return true;
|
||||
|
||||
// if we're in the middle of fetching results, don't let them send
|
||||
if (isFetching) return false;
|
||||
if (isFetching.value) return false;
|
||||
|
||||
// they're supposed to run IGC but haven't yet, don't let them send
|
||||
if (!igc.hasIGCTextData) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:matrix/matrix.dart' hide Result;
|
|||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/extensions/choregrapher_user_settings_extension.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/choreo_mode.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/pangea_match_status.dart';
|
||||
|
|
@ -53,21 +54,6 @@ class IgcController {
|
|||
|
||||
void clearMatches() => _igcTextData?.clearMatches();
|
||||
|
||||
PangeaMatchState? onShowFirstMatch() {
|
||||
if (!canShowFirstMatch) {
|
||||
throw "should not be calling showFirstMatch with this igcTextData.";
|
||||
}
|
||||
|
||||
final match = _igcTextData!.firstOpenMatch!;
|
||||
if (match.updatedMatch.isITStart && _igcTextData != null) {
|
||||
_choreographer.openIT(match);
|
||||
return null;
|
||||
}
|
||||
|
||||
_choreographer.chatController.inputFocus.unfocus();
|
||||
return match;
|
||||
}
|
||||
|
||||
PangeaMatchState? getMatchByOffset(int offset) =>
|
||||
_igcTextData?.getMatchByOffset(offset);
|
||||
|
||||
|
|
@ -125,10 +111,10 @@ class IgcController {
|
|||
);
|
||||
|
||||
if (res.isError) {
|
||||
_igcTextData = IGCTextData(
|
||||
originalInput: reqBody.fullText,
|
||||
matches: [],
|
||||
_choreographer.errorService.setErrorAndLock(
|
||||
ChoreoError(raw: res.asError),
|
||||
);
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -145,11 +131,6 @@ class IgcController {
|
|||
|
||||
try {
|
||||
_choreographer.acceptNormalizationMatches();
|
||||
if (_igcTextData != null) {
|
||||
for (final match in _igcTextData!.openMatches) {
|
||||
fetchSpanDetails(match: match);
|
||||
}
|
||||
}
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
|
|
@ -160,6 +141,12 @@ class IgcController {
|
|||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (_igcTextData != null) {
|
||||
for (final match in _igcTextData!.openMatches) {
|
||||
fetchSpanDetails(match: match).catchError((e) {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchSpanDetails({
|
||||
|
|
@ -190,8 +177,7 @@ class IgcController {
|
|||
);
|
||||
|
||||
if (response.isError) {
|
||||
_choreographer.clearMatches(response.error!);
|
||||
return;
|
||||
throw response.error!;
|
||||
}
|
||||
|
||||
_igcTextData?.setSpanData(match, response.result!.span);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ enum AssistanceState {
|
|||
fetching,
|
||||
fetched,
|
||||
complete,
|
||||
error,
|
||||
}
|
||||
|
||||
extension AssistanceStateExtension on AssistanceState {
|
||||
|
|
@ -21,6 +22,7 @@ extension AssistanceStateExtension on AssistanceState {
|
|||
case AssistanceState.noSub:
|
||||
case AssistanceState.noMessage:
|
||||
case AssistanceState.fetched:
|
||||
case AssistanceState.error:
|
||||
return Theme.of(context).disabledColor;
|
||||
case AssistanceState.notFetched:
|
||||
case AssistanceState.fetching:
|
||||
|
|
@ -29,4 +31,19 @@ extension AssistanceStateExtension on AssistanceState {
|
|||
return AppConfig.success;
|
||||
}
|
||||
}
|
||||
|
||||
Color sendButtonColor(context) {
|
||||
switch (this) {
|
||||
case AssistanceState.noMessage:
|
||||
case AssistanceState.fetched:
|
||||
return Theme.of(context).disabledColor;
|
||||
case AssistanceState.noSub:
|
||||
case AssistanceState.error:
|
||||
case AssistanceState.notFetched:
|
||||
case AssistanceState.fetching:
|
||||
return Theme.of(context).colorScheme.primary;
|
||||
case AssistanceState.complete:
|
||||
return AppConfig.success;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,14 +13,9 @@ import 'package:fluffychat/widgets/matrix.dart';
|
|||
import '../../bot/utils/bot_style.dart';
|
||||
import 'it_shimmer.dart';
|
||||
|
||||
// CTODO refactor
|
||||
typedef ChoiceCallback = void Function(String value, int index);
|
||||
|
||||
enum OverflowMode {
|
||||
wrap,
|
||||
horizontalScroll,
|
||||
verticalScroll,
|
||||
}
|
||||
|
||||
class ChoicesArray extends StatefulWidget {
|
||||
final bool isLoading;
|
||||
final List<Choice>? choices;
|
||||
|
|
@ -38,7 +33,7 @@ class ChoicesArray extends StatefulWidget {
|
|||
final String? id;
|
||||
|
||||
/// some uses of this widget want to disable clicking of the choices
|
||||
final bool isActive;
|
||||
final bool enabled;
|
||||
|
||||
final String Function(String)? getDisplayCopy;
|
||||
|
||||
|
|
@ -46,10 +41,6 @@ class ChoicesArray extends StatefulWidget {
|
|||
/// select choices once the correct choice has been selected
|
||||
final bool enableMultiSelect;
|
||||
|
||||
final double? fontSize;
|
||||
|
||||
final OverflowMode overflowMode;
|
||||
|
||||
const ChoicesArray({
|
||||
super.key,
|
||||
required this.isLoading,
|
||||
|
|
@ -58,13 +49,11 @@ class ChoicesArray extends StatefulWidget {
|
|||
required this.selectedChoiceIndex,
|
||||
this.enableAudio = true,
|
||||
this.langCode,
|
||||
this.isActive = true,
|
||||
this.enabled = true,
|
||||
this.onLongPress,
|
||||
this.getDisplayCopy,
|
||||
this.id,
|
||||
this.enableMultiSelect = false,
|
||||
this.fontSize,
|
||||
this.overflowMode = OverflowMode.wrap,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -99,8 +88,8 @@ class ChoicesArrayState extends State<ChoicesArray> {
|
|||
.mapIndexed(
|
||||
(index, entry) => ChoiceItem(
|
||||
theme: theme,
|
||||
onLongPress: widget.isActive ? widget.onLongPress : null,
|
||||
onPressed: widget.isActive
|
||||
onLongPress: widget.enabled ? widget.onLongPress : null,
|
||||
onPressed: widget.enabled
|
||||
? (String value, int index) {
|
||||
widget.onPressed(value, index);
|
||||
// TODO - what to pass here as eventID?
|
||||
|
|
@ -122,39 +111,18 @@ class ChoicesArrayState extends State<ChoicesArray> {
|
|||
isSelected: widget.selectedChoiceIndex == index,
|
||||
id: widget.id,
|
||||
getDisplayCopy: widget.getDisplayCopy,
|
||||
fontSize: widget.fontSize,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return widget.isLoading &&
|
||||
(widget.choices == null || widget.choices!.length <= 1)
|
||||
? ItShimmer(
|
||||
fontSize: widget.fontSize ??
|
||||
Theme.of(context).textTheme.bodyMedium?.fontSize ??
|
||||
16,
|
||||
)
|
||||
: widget.overflowMode == OverflowMode.wrap
|
||||
? Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 4.0,
|
||||
children: choices,
|
||||
)
|
||||
: widget.overflowMode == OverflowMode.horizontalScroll
|
||||
? SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: choices,
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: choices,
|
||||
),
|
||||
);
|
||||
? const ItShimmer()
|
||||
: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 4.0,
|
||||
children: choices,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,13 +12,12 @@ class AutocorrectPopup extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface.withAlpha(200),
|
||||
color: Theme.of(context).colorScheme.surface.withAlpha(200),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: Row(
|
||||
|
|
|
|||
|
|
@ -3,49 +3,38 @@ import 'package:flutter/material.dart';
|
|||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
|
||||
class CardErrorWidget extends StatelessWidget {
|
||||
final String error;
|
||||
final double maxWidth;
|
||||
|
||||
const CardErrorWidget({
|
||||
const CardErrorWidget(
|
||||
this.error, {
|
||||
super.key,
|
||||
required this.error,
|
||||
this.maxWidth = 275,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ErrorCopy errorCopy = ErrorCopy(
|
||||
context,
|
||||
title: L10n.of(context).oopsSomethingWentWrong,
|
||||
body: error,
|
||||
);
|
||||
|
||||
return Container(
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: Column(
|
||||
spacing: 6.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
errorCopy.title,
|
||||
L10n.of(context).oopsSomethingWentWrong,
|
||||
style: BotStyle.text(context),
|
||||
softWrap: true,
|
||||
),
|
||||
const SizedBox(height: 6.0),
|
||||
Row(
|
||||
spacing: 12.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const BotFace(
|
||||
width: 50.0,
|
||||
expression: BotExpression.addled,
|
||||
),
|
||||
const SizedBox(width: 12.0),
|
||||
Flexible(
|
||||
child: Text(
|
||||
errorCopy.body,
|
||||
error,
|
||||
style: BotStyle.text(context),
|
||||
softWrap: true,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -5,52 +5,41 @@ import 'package:fluffychat/widgets/matrix.dart';
|
|||
import '../../../bot/widgets/bot_face_svg.dart';
|
||||
|
||||
class CardHeader extends StatelessWidget {
|
||||
const CardHeader({
|
||||
const CardHeader(
|
||||
this.text, {
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.botExpression,
|
||||
this.onClose,
|
||||
});
|
||||
|
||||
final BotExpression botExpression;
|
||||
final String text;
|
||||
final void Function()? onClose;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 5.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: [
|
||||
BotFace(
|
||||
width: 50.0,
|
||||
expression: botExpression,
|
||||
return Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
const BotFace(
|
||||
width: 50.0,
|
||||
expression: BotExpression.addled,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: BotStyle.text(context),
|
||||
softWrap: true,
|
||||
),
|
||||
const SizedBox(width: 12.0),
|
||||
Flexible(
|
||||
child: Text(
|
||||
text,
|
||||
style: BotStyle.text(context),
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 5.0),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: () {
|
||||
if (onClose != null) onClose!();
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: MatrixState.pAnyState.closeOverlay,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,30 +2,13 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/card_header.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class LanguageMismatchPopup extends StatelessWidget {
|
||||
final String targetLanguage;
|
||||
final VoidCallback onUpdate;
|
||||
|
||||
const LanguageMismatchPopup({
|
||||
super.key,
|
||||
required this.targetLanguage,
|
||||
required this.onUpdate,
|
||||
});
|
||||
|
||||
Future<void> _onConfirm(BuildContext context) async {
|
||||
await MatrixState.pangeaController.userController.updateProfile(
|
||||
(profile) {
|
||||
profile.userSettings.targetLanguage = targetLanguage;
|
||||
return profile;
|
||||
},
|
||||
waitForDataInSync: true,
|
||||
);
|
||||
}
|
||||
final Future<void> Function() onConfirm;
|
||||
const LanguageMismatchPopup({super.key, required this.onConfirm});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -33,10 +16,7 @@ class LanguageMismatchPopup extends StatelessWidget {
|
|||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CardHeader(
|
||||
text: L10n.of(context).languageMismatchTitle,
|
||||
botExpression: BotExpression.addled,
|
||||
),
|
||||
CardHeader(L10n.of(context).languageMismatchTitle),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
|
|
@ -54,15 +34,13 @@ class LanguageMismatchPopup extends StatelessWidget {
|
|||
onPressed: () async {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => _onConfirm(context),
|
||||
future: onConfirm,
|
||||
);
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
onUpdate();
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
(Theme.of(context).colorScheme.primary).withAlpha(25),
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary.withAlpha(25),
|
||||
),
|
||||
child: Text(L10n.of(context).confirm),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -6,17 +6,16 @@ import 'package:fluffychat/config/themes.dart';
|
|||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class MessageAnalyticsFeedback extends StatefulWidget {
|
||||
final String overlayId;
|
||||
final int newGrammarConstructs;
|
||||
final int newVocabConstructs;
|
||||
final VoidCallback close;
|
||||
|
||||
const MessageAnalyticsFeedback({
|
||||
required this.overlayId,
|
||||
required this.newGrammarConstructs,
|
||||
required this.newVocabConstructs,
|
||||
required this.close,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -27,36 +26,27 @@ class MessageAnalyticsFeedback extends StatefulWidget {
|
|||
|
||||
class MessageAnalyticsFeedbackState extends State<MessageAnalyticsFeedback>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _vocabController;
|
||||
late AnimationController _grammarController;
|
||||
late AnimationController _numbersController;
|
||||
late AnimationController _bubbleController;
|
||||
late AnimationController _tickerController;
|
||||
|
||||
late Animation<double> _vocabOpacity;
|
||||
late Animation<double> _grammarOpacity;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _opacityAnimation;
|
||||
late Animation<double> _numbersOpacityAnimation;
|
||||
late Animation<double> _bubbleScaleAnimation;
|
||||
late Animation<double> _bubbleOpacityAnimation;
|
||||
|
||||
static const counterDelay = Duration(milliseconds: 400);
|
||||
Animation<int>? _grammarTickerAnimation;
|
||||
Animation<int>? _vocabTickerAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_grammarController = AnimationController(
|
||||
_numbersController = AnimationController(
|
||||
vsync: this,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
);
|
||||
|
||||
_grammarOpacity = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _grammarController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_vocabController = AnimationController(
|
||||
vsync: this,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
);
|
||||
|
||||
_vocabOpacity = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _vocabController, curve: Curves.easeInOut),
|
||||
_numbersOpacityAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _numbersController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_bubbleController = AnimationController(
|
||||
|
|
@ -64,51 +54,67 @@ class MessageAnalyticsFeedbackState extends State<MessageAnalyticsFeedback>
|
|||
duration: FluffyThemes.animationDuration,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
_bubbleScaleAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _bubbleController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_opacityAnimation = Tween<double>(begin: 0.0, end: 0.9).animate(
|
||||
_bubbleOpacityAnimation = Tween<double>(begin: 0.0, end: 0.9).animate(
|
||||
CurvedAnimation(parent: _bubbleController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _bubbleController.forward();
|
||||
_tickerController = AnimationController(
|
||||
vsync: this,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
);
|
||||
|
||||
Future.delayed(counterDelay, () {
|
||||
if (mounted) {
|
||||
_vocabController.forward();
|
||||
_grammarController.forward();
|
||||
}
|
||||
});
|
||||
_numbersOpacityAnimation.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
_startTickerAnimations();
|
||||
}
|
||||
});
|
||||
|
||||
Future.delayed(const Duration(milliseconds: 4000), () {
|
||||
if (!mounted) return;
|
||||
_bubbleController.reverse().then((_) {
|
||||
MatrixState.pAnyState.closeOverlay(widget.overlayId);
|
||||
});
|
||||
});
|
||||
_bubbleController.forward();
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 400),
|
||||
_numbersController.forward,
|
||||
);
|
||||
Future.delayed(const Duration(milliseconds: 4000), () async {
|
||||
await _bubbleController.reverse();
|
||||
if (mounted) widget.close();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_vocabController.dispose();
|
||||
_grammarController.dispose();
|
||||
_numbersController.dispose();
|
||||
_bubbleController.dispose();
|
||||
_tickerController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _showAnalyticsDialog(ConstructTypeEnum? type) {
|
||||
switch (type) {
|
||||
case ConstructTypeEnum.morph:
|
||||
context.go("/rooms/analytics/${ConstructTypeEnum.morph.string}");
|
||||
break;
|
||||
case ConstructTypeEnum.vocab:
|
||||
default:
|
||||
context.go("/rooms/analytics/${ConstructTypeEnum.vocab.string}");
|
||||
break;
|
||||
}
|
||||
void _startTickerAnimations() {
|
||||
_vocabTickerAnimation = IntTween(
|
||||
begin: 0,
|
||||
end: widget.newVocabConstructs,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _tickerController,
|
||||
curve: Curves.easeOutCubic,
|
||||
),
|
||||
);
|
||||
|
||||
_grammarTickerAnimation = IntTween(
|
||||
begin: 0,
|
||||
end: widget.newGrammarConstructs,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _tickerController,
|
||||
curve: Curves.easeOutCubic,
|
||||
),
|
||||
);
|
||||
|
||||
setState(() {});
|
||||
_tickerController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -116,22 +122,24 @@ class MessageAnalyticsFeedbackState extends State<MessageAnalyticsFeedback>
|
|||
if (widget.newVocabConstructs <= 0 && widget.newGrammarConstructs <= 0) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
// CTODO check if working
|
||||
|
||||
final theme = Theme.of(context);
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
onTap: () => _showAnalyticsDialog(null),
|
||||
onTap: () => context.go("/rooms/analytics"),
|
||||
child: ScaleTransition(
|
||||
scale: _scaleAnimation,
|
||||
scale: _bubbleScaleAnimation,
|
||||
alignment: Alignment.bottomRight,
|
||||
child: AnimatedBuilder(
|
||||
animation: _bubbleController,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest
|
||||
.withAlpha((_opacityAnimation.value * 255).round()),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHighest
|
||||
.withAlpha((_bubbleOpacityAnimation.value * 255).round()),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16.0),
|
||||
topRight: Radius.circular(16.0),
|
||||
|
|
@ -147,26 +155,18 @@ class MessageAnalyticsFeedbackState extends State<MessageAnalyticsFeedback>
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.newVocabConstructs > 0)
|
||||
NewConstructsBadge(
|
||||
controller: _vocabController,
|
||||
opacityAnimation: _vocabOpacity,
|
||||
newConstructs: widget.newVocabConstructs,
|
||||
_NewConstructsBadge(
|
||||
opacityAnimation: _numbersOpacityAnimation,
|
||||
tickerAnimation: _vocabTickerAnimation,
|
||||
type: ConstructTypeEnum.vocab,
|
||||
tooltip: L10n.of(context).newVocab,
|
||||
onTap: () => _showAnalyticsDialog(
|
||||
ConstructTypeEnum.vocab,
|
||||
),
|
||||
),
|
||||
if (widget.newGrammarConstructs > 0)
|
||||
NewConstructsBadge(
|
||||
controller: _grammarController,
|
||||
opacityAnimation: _grammarOpacity,
|
||||
newConstructs: widget.newGrammarConstructs,
|
||||
_NewConstructsBadge(
|
||||
opacityAnimation: _numbersOpacityAnimation,
|
||||
tickerAnimation: _grammarTickerAnimation,
|
||||
type: ConstructTypeEnum.morph,
|
||||
tooltip: L10n.of(context).newGrammar,
|
||||
onTap: () => _showAnalyticsDialog(
|
||||
ConstructTypeEnum.morph,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -179,33 +179,27 @@ class MessageAnalyticsFeedbackState extends State<MessageAnalyticsFeedback>
|
|||
}
|
||||
}
|
||||
|
||||
class NewConstructsBadge extends StatelessWidget {
|
||||
final AnimationController controller;
|
||||
class _NewConstructsBadge extends StatelessWidget {
|
||||
final Animation<double> opacityAnimation;
|
||||
|
||||
final int newConstructs;
|
||||
final Animation<int>? tickerAnimation;
|
||||
final ConstructTypeEnum type;
|
||||
final String tooltip;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const NewConstructsBadge({
|
||||
required this.controller,
|
||||
const _NewConstructsBadge({
|
||||
required this.opacityAnimation,
|
||||
required this.newConstructs,
|
||||
required this.tickerAnimation,
|
||||
required this.type,
|
||||
required this.tooltip,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
onTap: () => context.go("/rooms/analytics/${type.string}"),
|
||||
child: Tooltip(
|
||||
message: tooltip,
|
||||
child: AnimatedBuilder(
|
||||
animation: controller,
|
||||
animation: opacityAnimation,
|
||||
builder: (context, child) {
|
||||
return Opacity(
|
||||
opacity: opacityAnimation.value,
|
||||
|
|
@ -220,10 +214,9 @@ class NewConstructsBadge extends StatelessWidget {
|
|||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
AnimatedCounter(
|
||||
_AnimatedCounter(
|
||||
key: ValueKey("$type-counter"),
|
||||
endValue: newConstructs,
|
||||
startAnimation: opacityAnimation.value > 0.9,
|
||||
animation: tickerAnimation,
|
||||
style: TextStyle(
|
||||
color: type.indicator.color(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -240,76 +233,31 @@ class NewConstructsBadge extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class AnimatedCounter extends StatefulWidget {
|
||||
final int endValue;
|
||||
class _AnimatedCounter extends StatelessWidget {
|
||||
final Animation<int>? animation;
|
||||
final TextStyle? style;
|
||||
final bool startAnimation;
|
||||
|
||||
const AnimatedCounter({
|
||||
const _AnimatedCounter({
|
||||
super.key,
|
||||
required this.endValue,
|
||||
required this.animation,
|
||||
this.style,
|
||||
this.startAnimation = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AnimatedCounter> createState() => _AnimatedCounterState();
|
||||
}
|
||||
|
||||
class _AnimatedCounterState extends State<AnimatedCounter>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<int> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
);
|
||||
|
||||
_animation = IntTween(
|
||||
begin: 0,
|
||||
end: widget.endValue,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOutCubic,
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.startAnimation) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _controller.forward();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(AnimatedCounter oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (!oldWidget.startAnimation && widget.startAnimation && !_hasAnimated) {
|
||||
_controller.forward();
|
||||
}
|
||||
}
|
||||
|
||||
bool get _hasAnimated => _controller.isCompleted || _controller.isAnimating;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (animation == null) {
|
||||
return Text(
|
||||
"+ 0",
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _animation,
|
||||
animation: animation!,
|
||||
builder: (context, child) {
|
||||
return Text(
|
||||
"+ ${_animation.value}",
|
||||
style: widget.style,
|
||||
"+ ${animation!.value}",
|
||||
style: style,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,16 +2,13 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/card_header.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/overlay.dart';
|
||||
import 'package:fluffychat/pangea/subscription/repo/subscription_management_repo.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class PaywallCard extends StatelessWidget {
|
||||
const PaywallCard({
|
||||
super.key,
|
||||
});
|
||||
const PaywallCard({super.key});
|
||||
|
||||
static Future<void> show(
|
||||
BuildContext context,
|
||||
|
|
@ -34,71 +31,33 @@ class PaywallCard extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool inTrialWindow =
|
||||
MatrixState.pangeaController.userController.inTrialWindow();
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
CardHeader(
|
||||
text: L10n.of(context).clickMessageTitle,
|
||||
botExpression: BotExpression.addled,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).subscribedToUnlockTools,
|
||||
style: BotStyle.text(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
// if (inTrialWindow)
|
||||
// Text(
|
||||
// L10n.of(context).noPaymentInfo,
|
||||
// style: BotStyle.text(context),
|
||||
// textAlign: TextAlign.center,
|
||||
// ),
|
||||
if (inTrialWindow) ...[
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
MatrixState.pangeaController.subscriptionController
|
||||
.activateNewUserTrial();
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
(Theme.of(context).colorScheme.primary).withAlpha(25),
|
||||
),
|
||||
),
|
||||
child: Text(L10n.of(context).activateTrial),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
MatrixState.pangeaController.subscriptionController
|
||||
.showPaywall(context);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
(Theme.of(context).colorScheme.primary).withAlpha(25),
|
||||
),
|
||||
),
|
||||
child: Text(L10n.of(context).getAccess),
|
||||
CardHeader(L10n.of(context).clickMessageTitle),
|
||||
Column(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).subscribedToUnlockTools,
|
||||
style: BotStyle.text(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
MatrixState.pangeaController.subscriptionController
|
||||
.showPaywall(context);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary.withAlpha(25),
|
||||
),
|
||||
child: Text(L10n.of(context).getAccess),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
|
|
@ -11,14 +12,13 @@ import 'package:fluffychat/pangea/choreographer/enums/span_data_type.dart';
|
|||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_state.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/span_data.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/feedback_model.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
||||
import '../../../../widgets/matrix.dart';
|
||||
import '../../../bot/widgets/bot_face_svg.dart';
|
||||
import '../choice_array.dart';
|
||||
import 'why_button.dart';
|
||||
|
||||
// CTODO refactor
|
||||
class SpanCard extends StatefulWidget {
|
||||
final PangeaMatchState match;
|
||||
final Choreographer choreographer;
|
||||
|
|
@ -34,57 +34,103 @@ class SpanCard extends StatefulWidget {
|
|||
}
|
||||
|
||||
class SpanCardState extends State<SpanCard> {
|
||||
bool fetchingData = false;
|
||||
bool _loadingChoices = true;
|
||||
final _feedbackModel = FeedbackModel<String>();
|
||||
|
||||
SpanChoice? _latestSelectedChoice;
|
||||
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
getSpanDetails();
|
||||
_fetchChoices();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
TtsController.stop();
|
||||
_feedbackModel.dispose();
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
SpanChoice? get selectedChoice =>
|
||||
widget.match.updatedMatch.match.selectedChoice;
|
||||
List<SpanChoice>? get _choices => widget.match.updatedMatch.match.choices;
|
||||
|
||||
Future<void> getSpanDetails({bool force = false}) async {
|
||||
if (widget.match.updatedMatch.isITStart) return;
|
||||
SpanChoice? get _selectedChoice =>
|
||||
widget.match.updatedMatch.match.selectedChoice ??
|
||||
widget.match.updatedMatch.match.choices?.firstWhereOrNull(
|
||||
(c) => c.value == _latestSelectedChoice?.value,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
fetchingData = true;
|
||||
});
|
||||
String? get _selectedFeedback => _selectedChoice?.feedback;
|
||||
|
||||
await widget.choreographer.fetchSpanDetails(
|
||||
match: widget.match,
|
||||
force: force,
|
||||
);
|
||||
Future<void> _fetchChoices() async {
|
||||
if (_choices != null && _choices!.length > 1) {
|
||||
setState(() => _loadingChoices = false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() => fetchingData = false);
|
||||
try {
|
||||
setState(() => _loadingChoices = true);
|
||||
await widget.choreographer.fetchSpanDetails(
|
||||
match: widget.match,
|
||||
);
|
||||
} catch (e) {
|
||||
widget.choreographer.clearMatches(e);
|
||||
} finally {
|
||||
if (_choices == null || _choices!.isEmpty) {
|
||||
widget.choreographer.clearMatches(
|
||||
'No choices available for span ${widget.match.updatedMatch.match.message}',
|
||||
);
|
||||
}
|
||||
setState(() => _loadingChoices = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _fetchFeedback() async {
|
||||
if (_selectedFeedback != null) {
|
||||
_feedbackModel.setState(FeedbackLoaded<String>(_selectedFeedback!));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
_feedbackModel.setState(FeedbackLoading<String>());
|
||||
await widget.choreographer.fetchSpanDetails(
|
||||
match: widget.match,
|
||||
force: true,
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
if (_selectedFeedback == null) {
|
||||
_feedbackModel.setState(
|
||||
FeedbackError<String>(
|
||||
L10n.of(context).failedToLoadFeedback,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
_feedbackModel.setState(
|
||||
FeedbackLoaded<String>(_selectedFeedback!),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onChoiceSelect(int index) {
|
||||
final selected = _choices![index];
|
||||
widget.match.selectChoice(index);
|
||||
setState(
|
||||
() => (selectedChoice!.isBestCorrection
|
||||
? BotExpression.gold
|
||||
: BotExpression.surprised),
|
||||
_latestSelectedChoice = selected;
|
||||
_feedbackModel.setState(
|
||||
selected.feedback != null
|
||||
? FeedbackLoaded<String>(selected.feedback!)
|
||||
: FeedbackIdle<String>(),
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _onAcceptReplacement() async {
|
||||
void _onMatchUpdate(VoidCallback updateFunc) async {
|
||||
try {
|
||||
widget.choreographer.onAcceptReplacement(
|
||||
match: widget.match,
|
||||
);
|
||||
updateFunc();
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
|
|
@ -97,34 +143,21 @@ class SpanCardState extends State<SpanCard> {
|
|||
widget.choreographer.clearMatches(e);
|
||||
return;
|
||||
}
|
||||
|
||||
_showFirstMatch();
|
||||
}
|
||||
|
||||
void _onIgnoreMatch() {
|
||||
try {
|
||||
widget.choreographer.onIgnoreMatch(match: widget.match);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
level: SentryLevel.warning,
|
||||
data: {
|
||||
"match": widget.match.toJson(),
|
||||
},
|
||||
);
|
||||
widget.choreographer.clearMatches(e);
|
||||
return;
|
||||
}
|
||||
void _onAcceptReplacement() => _onMatchUpdate(() {
|
||||
widget.choreographer.onAcceptReplacement(match: widget.match);
|
||||
});
|
||||
|
||||
_showFirstMatch();
|
||||
}
|
||||
void _onIgnoreMatch() => _onMatchUpdate(() {
|
||||
widget.choreographer.onIgnoreMatch(match: widget.match);
|
||||
});
|
||||
|
||||
void _showFirstMatch() {
|
||||
if (widget.choreographer.canShowFirstIGCMatch) {
|
||||
final igcMatch = widget.choreographer.igc.onShowFirstMatch();
|
||||
OverlayUtil.showIGCMatch(
|
||||
igcMatch!,
|
||||
widget.choreographer.igc.firstOpenMatch!,
|
||||
widget.choreographer,
|
||||
context,
|
||||
);
|
||||
|
|
@ -135,29 +168,6 @@ class SpanCardState extends State<SpanCard> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WordMatchContent(
|
||||
controller: this,
|
||||
scrollController: scrollController,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WordMatchContent extends StatelessWidget {
|
||||
final SpanCardState controller;
|
||||
final ScrollController scrollController;
|
||||
|
||||
const WordMatchContent({
|
||||
required this.controller,
|
||||
required this.scrollController,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (controller.widget.match.updatedMatch.isITStart) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: 300.0,
|
||||
child: Column(
|
||||
|
|
@ -167,34 +177,81 @@ class WordMatchContent extends StatelessWidget {
|
|||
controller: scrollController,
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
ChoicesArray(
|
||||
isLoading: controller.fetchingData,
|
||||
choices:
|
||||
controller.widget.match.updatedMatch.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
|
||||
.widget.match.updatedMatch.match.selectedChoiceIndex,
|
||||
id: controller.widget.match.hashCode.toString(),
|
||||
langCode: MatrixState.pangeaController.languageController
|
||||
.activeL2Code(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
PromptAndFeedback(controller: controller),
|
||||
],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: Column(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
ChoicesArray(
|
||||
isLoading: _loadingChoices,
|
||||
choices: widget.match.updatedMatch.match.choices
|
||||
?.map(
|
||||
(e) => Choice(
|
||||
text: e.value,
|
||||
color: e.selected ? e.type.color : null,
|
||||
isGold: e.type.name == 'bestCorrection',
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onPressed: (value, index) => _onChoiceSelect(index),
|
||||
selectedChoiceIndex:
|
||||
widget.match.updatedMatch.match.selectedChoiceIndex,
|
||||
id: widget.match.hashCode.toString(),
|
||||
langCode: MatrixState
|
||||
.pangeaController.languageController
|
||||
.activeL2Code(),
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 100.0,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ListenableBuilder(
|
||||
listenable: _feedbackModel,
|
||||
builder: (context, _) {
|
||||
if (_loadingChoices) {
|
||||
return const SizedBox(
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
final state = _feedbackModel.state;
|
||||
return switch (state) {
|
||||
FeedbackIdle<String>() =>
|
||||
_selectedChoice == null
|
||||
? Text(
|
||||
widget.match.updatedMatch.match.type
|
||||
.typeName
|
||||
.defaultPrompt(context),
|
||||
style:
|
||||
BotStyle.text(context).copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
)
|
||||
: WhyButton(
|
||||
onPress: _fetchFeedback,
|
||||
loading: false,
|
||||
),
|
||||
FeedbackLoading<String>() => WhyButton(
|
||||
onPress: _fetchFeedback,
|
||||
loading: true,
|
||||
),
|
||||
FeedbackError<String>(:final error) =>
|
||||
ErrorIndicator(message: error.toString()),
|
||||
FeedbackLoaded<String>(:final value) =>
|
||||
Text(value, style: BotStyle.text(context)),
|
||||
};
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -203,7 +260,7 @@ class WordMatchContent extends StatelessWidget {
|
|||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
),
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
padding: const EdgeInsets.only(top: 12.0),
|
||||
child: Row(
|
||||
spacing: 10.0,
|
||||
children: [
|
||||
|
|
@ -211,12 +268,11 @@ class WordMatchContent extends StatelessWidget {
|
|||
child: Opacity(
|
||||
opacity: 0.8,
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
Theme.of(context).colorScheme.primary.withAlpha(25),
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary.withAlpha(25),
|
||||
),
|
||||
onPressed: controller._onIgnoreMatch,
|
||||
onPressed: _onIgnoreMatch,
|
||||
child: Center(
|
||||
child: Text(L10n.of(context).ignoreInThisText),
|
||||
),
|
||||
|
|
@ -225,26 +281,19 @@ class WordMatchContent extends StatelessWidget {
|
|||
),
|
||||
Expanded(
|
||||
child: Opacity(
|
||||
opacity: controller.selectedChoice != null ? 1.0 : 0.5,
|
||||
opacity: _selectedChoice != null ? 1.0 : 0.5,
|
||||
child: TextButton(
|
||||
onPressed: controller.selectedChoice != null
|
||||
? controller._onAcceptReplacement
|
||||
: 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,
|
||||
),
|
||||
onPressed:
|
||||
_selectedChoice != null ? _onAcceptReplacement : null,
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: (_selectedChoice?.color ??
|
||||
Theme.of(context).colorScheme.primary)
|
||||
.withAlpha(50),
|
||||
side: _selectedChoice != null
|
||||
? BorderSide(
|
||||
color: _selectedChoice!.color,
|
||||
style: BorderStyle.solid,
|
||||
width: 2.0,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
|
|
@ -260,60 +309,3 @@ class WordMatchContent extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PromptAndFeedback extends StatelessWidget {
|
||||
const PromptAndFeedback({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
final SpanCardState controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: controller.widget.match.updatedMatch.isITStart
|
||||
? null
|
||||
: const BoxConstraints(minHeight: 75.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (controller.selectedChoice == null && controller.fetchingData)
|
||||
const Center(
|
||||
child: SizedBox(
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
if (controller.selectedChoice != null) ...[
|
||||
if (controller.selectedChoice?.feedback != null)
|
||||
Text(
|
||||
controller.selectedChoice!.feedbackToDisplay(context),
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (controller.selectedChoice?.feedback == null)
|
||||
WhyButton(
|
||||
onPress: () {
|
||||
if (!controller.fetchingData) {
|
||||
controller.getSpanDetails(force: true);
|
||||
}
|
||||
},
|
||||
loading: controller.fetchingData,
|
||||
),
|
||||
],
|
||||
if (!controller.fetchingData && controller.selectedChoice == null)
|
||||
Text(
|
||||
controller.widget.match.updatedMatch.match.type.typeName
|
||||
.defaultPrompt(context),
|
||||
style: BotStyle.text(context).copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,21 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:async/async.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/contextual_definition_repo.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/contextual_definition_request_model.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/contextual_definition_response_model.dart';
|
||||
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/feedback_model.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/toolbar_content_loading_indicator.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'card_error_widget.dart';
|
||||
|
||||
class WordDataCard extends StatefulWidget {
|
||||
final String word;
|
||||
final String fullText;
|
||||
final String? choiceFeedback;
|
||||
final String wordLang;
|
||||
final String fullTextLang;
|
||||
|
||||
|
|
@ -27,7 +23,6 @@ class WordDataCard extends StatefulWidget {
|
|||
super.key,
|
||||
required this.word,
|
||||
required this.fullText,
|
||||
this.choiceFeedback,
|
||||
required this.wordLang,
|
||||
required this.fullTextLang,
|
||||
});
|
||||
|
|
@ -37,137 +32,88 @@ class WordDataCard extends StatefulWidget {
|
|||
}
|
||||
|
||||
class WordDataCardController extends State<WordDataCard> {
|
||||
final PangeaController controller = MatrixState.pangeaController;
|
||||
|
||||
bool isLoadingContextualDefinition = false;
|
||||
ContextualDefinitionResponseModel? contextualDefinitionRes;
|
||||
|
||||
Object? definitionError;
|
||||
LanguageModel? activeL1;
|
||||
LanguageModel? activeL2;
|
||||
|
||||
Response get noLanguages => Response("", 405);
|
||||
final FeedbackModel<String> _feedbackModel = FeedbackModel<String>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (!mounted) return;
|
||||
activeL1 = controller.languageController.activeL1Model()!;
|
||||
activeL2 = controller.languageController.activeL2Model()!;
|
||||
if (activeL1 == null || activeL2 == null) {
|
||||
definitionError = noLanguages;
|
||||
} else {
|
||||
getContextualDefinition();
|
||||
}
|
||||
super.initState();
|
||||
_getContextualDefinition();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant WordDataCard oldWidget) {
|
||||
// debugger(when: kDebugMode);
|
||||
if (oldWidget.word != widget.word) {
|
||||
getContextualDefinition();
|
||||
_getContextualDefinition();
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
Future<void> getContextualDefinition() async {
|
||||
final ContextualDefinitionRequestModel req =
|
||||
ContextualDefinitionRequestModel(
|
||||
fullText: widget.fullText,
|
||||
word: widget.word,
|
||||
feedbackLang: activeL1?.langCode ?? LanguageKeys.defaultLanguage,
|
||||
fullTextLang: widget.fullTextLang,
|
||||
wordLang: widget.wordLang,
|
||||
);
|
||||
if (!mounted) return;
|
||||
@override
|
||||
void dispose() {
|
||||
_feedbackModel.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
contextualDefinitionRes = null;
|
||||
definitionError = null;
|
||||
isLoadingContextualDefinition = true;
|
||||
});
|
||||
ContextualDefinitionRequestModel get _request =>
|
||||
ContextualDefinitionRequestModel(
|
||||
fullText: widget.fullText,
|
||||
word: widget.word,
|
||||
fullTextLang: widget.fullTextLang,
|
||||
wordLang: widget.wordLang,
|
||||
feedbackLang:
|
||||
MatrixState.pangeaController.languageController.activeL1Code() ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
);
|
||||
|
||||
Future<void> _getContextualDefinition() async {
|
||||
_feedbackModel.setState(FeedbackLoading<String>());
|
||||
final resp = await ContextualDefinitionRepo.get(
|
||||
MatrixState.pangeaController.userController.accessToken,
|
||||
req,
|
||||
_request,
|
||||
).timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
return Result.error("Timeout getting definition");
|
||||
},
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
if (resp.isError) {
|
||||
definitionError = Exception("Error getting definition");
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() => isLoadingContextualDefinition = false);
|
||||
_feedbackModel.setState(
|
||||
const FeedbackError<String>("Error getting definition"),
|
||||
);
|
||||
} else {
|
||||
_feedbackModel.setState(FeedbackLoaded<String>(resp.result!.text));
|
||||
}
|
||||
}
|
||||
|
||||
void handleGetDefinitionButtonPress() {
|
||||
if (isLoadingContextualDefinition) return;
|
||||
getContextualDefinition();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => WordDataCardView(controller: this);
|
||||
}
|
||||
|
||||
class WordDataCardView extends StatelessWidget {
|
||||
const WordDataCardView({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
final WordDataCardController controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (controller.activeL1 == null || controller.activeL2 == null) {
|
||||
ErrorHandler.logError(
|
||||
m: "should not be here",
|
||||
data: {
|
||||
"activeL1": controller.activeL1?.toJson(),
|
||||
"activeL2": controller.activeL2?.toJson(),
|
||||
},
|
||||
);
|
||||
return CardErrorWidget(
|
||||
error: L10n.of(context).noLanguagesSet,
|
||||
maxWidth: AppConfig.toolbarMinWidth,
|
||||
);
|
||||
}
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: AppConfig.toolbarMinWidth,
|
||||
maxHeight: AppConfig.toolbarMaxHeight,
|
||||
),
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
children: [
|
||||
if (controller.widget.choiceFeedback != null)
|
||||
Text(
|
||||
controller.widget.choiceFeedback!,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
if (controller.isLoadingContextualDefinition)
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: ListenableBuilder(
|
||||
listenable: _feedbackModel,
|
||||
builder: (context, _) {
|
||||
final state = _feedbackModel.state;
|
||||
return switch (state) {
|
||||
FeedbackIdle<String>() => const SizedBox.shrink(),
|
||||
FeedbackLoading<String>() =>
|
||||
const ToolbarContentLoadingIndicator(),
|
||||
if (controller.contextualDefinitionRes != null)
|
||||
Text(
|
||||
controller.contextualDefinitionRes!.text,
|
||||
style: BotStyle.text(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (controller.definitionError != null)
|
||||
Text(
|
||||
FeedbackError<String>() => Text(
|
||||
L10n.of(context).sorryNoResults,
|
||||
style: BotStyle.text(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
FeedbackLoaded<String>(:final value) =>
|
||||
Text(value, style: BotStyle.text(context)),
|
||||
};
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,27 +2,24 @@ import 'dart:math';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:async/async.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/full_text_translation_repo.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/full_text_translation_request_model.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/full_text_translation_response_model.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/feedback_model.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import '../../../widgets/matrix.dart';
|
||||
import '../../bot/utils/bot_style.dart';
|
||||
import '../../common/controllers/pangea_controller.dart';
|
||||
import 'igc/card_error_widget.dart';
|
||||
|
||||
class ITFeedbackCard extends StatefulWidget {
|
||||
final FullTextTranslationRequestModel req;
|
||||
final String choiceFeedback;
|
||||
|
||||
const ITFeedbackCard({
|
||||
const ITFeedbackCard(
|
||||
this.req, {
|
||||
super.key,
|
||||
required this.req,
|
||||
required this.choiceFeedback,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -30,92 +27,84 @@ class ITFeedbackCard extends StatefulWidget {
|
|||
}
|
||||
|
||||
class ITFeedbackCardController extends State<ITFeedbackCard> {
|
||||
final PangeaController controller = MatrixState.pangeaController;
|
||||
|
||||
Object? error;
|
||||
bool isLoadingFeedback = false;
|
||||
bool isTranslating = false;
|
||||
FullTextTranslationResponseModel? res;
|
||||
String? translatedFeedback;
|
||||
|
||||
Response get noLanguages => Response("", 405);
|
||||
final FeedbackModel<String> _feedbackModel = FeedbackModel<String>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (!mounted) return;
|
||||
//any setup?
|
||||
super.initState();
|
||||
getFeedback();
|
||||
_getFeedback();
|
||||
}
|
||||
|
||||
Future<void> getFeedback() async {
|
||||
setState(() {
|
||||
isLoadingFeedback = true;
|
||||
});
|
||||
@override
|
||||
void dispose() {
|
||||
_feedbackModel.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _getFeedback() async {
|
||||
_feedbackModel.setState(FeedbackLoading());
|
||||
final result = await FullTextTranslationRepo.get(
|
||||
controller.userController.accessToken,
|
||||
MatrixState.pangeaController.userController.accessToken,
|
||||
widget.req,
|
||||
).timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
return Result.error("Timeout getting translation");
|
||||
},
|
||||
);
|
||||
res = result.result;
|
||||
|
||||
if (result.isError) error = result.error;
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isLoadingFeedback = false;
|
||||
});
|
||||
if (!mounted) return;
|
||||
if (result.isError) {
|
||||
_feedbackModel.setState(
|
||||
FeedbackError<String>(result.error.toString()),
|
||||
);
|
||||
} else {
|
||||
_feedbackModel.setState(
|
||||
FeedbackLoaded<String>(result.result!.bestTranslation),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => error == null
|
||||
? ITFeedbackCardView(controller: this)
|
||||
: CardErrorWidget(
|
||||
error: L10n.of(context).errorFetchingDefinition,
|
||||
);
|
||||
}
|
||||
|
||||
class ITFeedbackCardView extends StatelessWidget {
|
||||
const ITFeedbackCardView({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
final ITFeedbackCardController controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const characterWidth = 10.0;
|
||||
return ListenableBuilder(
|
||||
listenable: _feedbackModel,
|
||||
builder: (context, _) {
|
||||
final state = _feedbackModel.state;
|
||||
if (state is FeedbackError) {
|
||||
return CardErrorWidget(L10n.of(context).errorFetchingDefinition);
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxWidth: 300),
|
||||
alignment: Alignment.center,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
controller.widget.req.text,
|
||||
style: BotStyle.text(context),
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxWidth: 300),
|
||||
alignment: Alignment.center,
|
||||
child: Wrap(
|
||||
spacing: 10,
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
widget.req.text,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
Text(
|
||||
"≈",
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
_feedbackModel.state is FeedbackLoaded
|
||||
? Text(
|
||||
(state as FeedbackLoaded<String>).value,
|
||||
style: BotStyle.text(context),
|
||||
)
|
||||
: TextLoadingShimmer(
|
||||
width: min(
|
||||
140,
|
||||
10.0 * widget.req.text.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
"≈",
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
controller.res?.bestTranslation != null
|
||||
? Text(
|
||||
controller.res!.bestTranslation,
|
||||
style: BotStyle.text(context),
|
||||
)
|
||||
: TextLoadingShimmer(
|
||||
width: min(
|
||||
140,
|
||||
characterWidth * controller.widget.req.text.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,89 +3,51 @@ import 'dart:ui';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class ItShimmer extends StatelessWidget {
|
||||
const ItShimmer({
|
||||
super.key,
|
||||
required this.fontSize,
|
||||
});
|
||||
const ItShimmer({super.key});
|
||||
|
||||
final double fontSize;
|
||||
|
||||
Iterable<Widget> renderShimmerIfListEmpty(
|
||||
BuildContext context, {
|
||||
int noOfBars = 3,
|
||||
}) {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<String> dummyStrings = [];
|
||||
for (int i = 0; i < noOfBars; i++) {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
dummyStrings.add(" " * 10);
|
||||
}
|
||||
return dummyStrings.map(
|
||||
(e) => ITShimmerElement(
|
||||
text: e,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// PTODO - bring this back, make it shimmer
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
children: [...renderShimmerIfListEmpty(context, noOfBars: 3)],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ITShimmerElement extends StatelessWidget {
|
||||
const ITShimmerElement({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.fontSize,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final double fontSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(minWidth: 50),
|
||||
margin: const EdgeInsets.all(2),
|
||||
padding: EdgeInsets.zero,
|
||||
// decoration: BoxDecoration(
|
||||
// borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
// border: Border.all(
|
||||
// color: Theme.of(context).colorScheme.primary,
|
||||
// style: BorderStyle.solid,
|
||||
// width: 2.0,
|
||||
// ),
|
||||
// ),
|
||||
child: ImageFiltered(
|
||||
imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 7),
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
children: [
|
||||
...dummyStrings.map(
|
||||
(e) => Container(
|
||||
constraints: const BoxConstraints(minWidth: 50),
|
||||
margin: const EdgeInsets.all(2),
|
||||
padding: EdgeInsets.zero,
|
||||
child: ImageFiltered(
|
||||
imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 7),
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
Theme.of(context).colorScheme.primary.withAlpha(50),
|
||||
),
|
||||
),
|
||||
onPressed: null,
|
||||
child: Text(
|
||||
e,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Colors.transparent,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
Theme.of(context).colorScheme.primary.withAlpha(50),
|
||||
),
|
||||
),
|
||||
onPressed: null,
|
||||
child: Text(
|
||||
text,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(color: Colors.transparent, fontSize: fontSize),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,122 +0,0 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/extensions/choregrapher_user_settings_extension.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/pages/settings_learning.dart';
|
||||
|
||||
class ErrorCopy {
|
||||
final String title;
|
||||
final String? description;
|
||||
|
||||
ErrorCopy(this.title, [this.description]);
|
||||
}
|
||||
|
||||
class LanguagePermissionsButtons extends StatelessWidget {
|
||||
final String? roomID;
|
||||
final Choreographer choreographer;
|
||||
|
||||
const LanguagePermissionsButtons({
|
||||
super.key,
|
||||
required this.roomID,
|
||||
required this.choreographer,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (roomID == null) return const SizedBox.shrink();
|
||||
final ErrorCopy? copy = getCopy(context);
|
||||
if (copy == null) return const SizedBox.shrink();
|
||||
|
||||
final Widget text = RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: copy.title,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
),
|
||||
),
|
||||
if (copy.description != null)
|
||||
TextSpan(
|
||||
text: copy.description,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (c) => const SettingsLearning(),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return FloatingActionButton(
|
||||
mini: true,
|
||||
child: const Icon(Icons.history_edu_outlined),
|
||||
onPressed: () => showMessage(context, text),
|
||||
);
|
||||
}
|
||||
|
||||
ErrorCopy? getCopy(BuildContext context) {
|
||||
final bool itDisabled = !choreographer.itEnabled;
|
||||
final bool igcDisabled = !choreographer.igcEnabled;
|
||||
if (roomID == null) {
|
||||
ErrorHandler.logError(
|
||||
e: Exception("Room ID is null in language permissions"),
|
||||
data: {},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (igcDisabled && itDisabled) {
|
||||
return ErrorCopy(
|
||||
L10n.of(context).errorDisableLanguageAssistance,
|
||||
" ${L10n.of(context).errorDisableLanguageAssistanceUserDesc}",
|
||||
);
|
||||
}
|
||||
|
||||
if (itDisabled) {
|
||||
return ErrorCopy(
|
||||
L10n.of(context).errorDisableIT,
|
||||
" ${L10n.of(context).errorDisableITUserDesc}",
|
||||
);
|
||||
}
|
||||
|
||||
if (igcDisabled) {
|
||||
return ErrorCopy(
|
||||
L10n.of(context).errorDisableIGC,
|
||||
" ${L10n.of(context).errorDisableIGCUserDesc}",
|
||||
);
|
||||
}
|
||||
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: Exception("Unhandled case in language permissions"),
|
||||
data: {
|
||||
"roomID": roomID,
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
void showMessage(BuildContext context, Widget text) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
duration: const Duration(seconds: 10),
|
||||
content: text,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -26,12 +26,17 @@ class ChoreographerSendButton extends StatelessWidget {
|
|||
controller.choreographer.inputTransformTargetKey,
|
||||
);
|
||||
} on OpenMatchesException {
|
||||
if (controller.choreographer.firstIGCMatch != null) {
|
||||
OverlayUtil.showIGCMatch(
|
||||
controller.choreographer.firstIGCMatch!,
|
||||
controller.choreographer,
|
||||
context,
|
||||
);
|
||||
if (controller.choreographer.firstOpenMatch != null) {
|
||||
if (controller.choreographer.firstOpenMatch!.updatedMatch.isITStart) {
|
||||
controller.choreographer
|
||||
.openIT(controller.choreographer.firstOpenMatch!);
|
||||
} else {
|
||||
OverlayUtil.showIGCMatch(
|
||||
controller.choreographer.firstOpenMatch!,
|
||||
controller.choreographer,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -39,25 +44,23 @@ class ChoreographerSendButton extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: controller.choreographer,
|
||||
listenable: Listenable.merge([
|
||||
controller.choreographer.textController,
|
||||
controller.choreographer.isFetching,
|
||||
]),
|
||||
builder: (context, _) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: controller.choreographer.textController,
|
||||
builder: (context, _, __) {
|
||||
return Container(
|
||||
height: 56,
|
||||
alignment: Alignment.center,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.send_outlined),
|
||||
color: controller.choreographer.assistanceState
|
||||
.stateColor(context),
|
||||
onPressed: controller.choreographer.isFetching
|
||||
? null
|
||||
: () => _onPressed(context),
|
||||
tooltip: L10n.of(context).send,
|
||||
),
|
||||
);
|
||||
},
|
||||
return Container(
|
||||
height: 56,
|
||||
alignment: Alignment.center,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.send_outlined),
|
||||
color: controller.choreographer.assistanceState
|
||||
.sendButtonColor(context),
|
||||
onPressed: controller.choreographer.isFetching.value
|
||||
? null
|
||||
: () => _onPressed(context),
|
||||
tooltip: L10n.of(context).send,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -63,13 +63,17 @@ class StartIGCButtonState extends State<StartIGCButton>
|
|||
|
||||
void _showFirstMatch() {
|
||||
if (widget.controller.choreographer.canShowFirstIGCMatch) {
|
||||
final match = widget.controller.choreographer.igc.onShowFirstMatch();
|
||||
final match = widget.controller.choreographer.igc.firstOpenMatch;
|
||||
if (match == null) return;
|
||||
OverlayUtil.showIGCMatch(
|
||||
match,
|
||||
widget.controller.choreographer,
|
||||
context,
|
||||
);
|
||||
if (match.updatedMatch.isITStart) {
|
||||
widget.controller.choreographer.openIT(match);
|
||||
} else {
|
||||
OverlayUtil.showIGCMatch(
|
||||
match,
|
||||
widget.controller.choreographer,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -79,6 +83,8 @@ class StartIGCButtonState extends State<StartIGCButton>
|
|||
AssistanceState.fetched,
|
||||
AssistanceState.complete,
|
||||
AssistanceState.noMessage,
|
||||
AssistanceState.noSub,
|
||||
AssistanceState.error,
|
||||
].contains(assistanceState);
|
||||
}
|
||||
|
||||
|
|
@ -101,15 +107,18 @@ class StartIGCButtonState extends State<StartIGCButton>
|
|||
if (widget.controller.shouldShowLanguageMismatchPopup) {
|
||||
widget.controller.showLanguageMismatchPopup();
|
||||
} else {
|
||||
final igcMatch =
|
||||
await widget.controller.choreographer.requestLanguageAssistance();
|
||||
|
||||
if (igcMatch != null) {
|
||||
OverlayUtil.showIGCMatch(
|
||||
igcMatch,
|
||||
widget.controller.choreographer,
|
||||
context,
|
||||
);
|
||||
await widget.controller.choreographer.requestLanguageAssistance();
|
||||
final openMatch = widget.controller.choreographer.firstOpenMatch;
|
||||
if (openMatch != null) {
|
||||
if (openMatch.updatedMatch.isITStart) {
|
||||
widget.controller.choreographer.openIT(openMatch);
|
||||
} else {
|
||||
OverlayUtil.showIGCMatch(
|
||||
openMatch,
|
||||
widget.controller.choreographer,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
|
@ -118,6 +127,7 @@ class StartIGCButtonState extends State<StartIGCButton>
|
|||
return;
|
||||
case AssistanceState.complete:
|
||||
case AssistanceState.fetching:
|
||||
case AssistanceState.error:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -128,6 +138,7 @@ class StartIGCButtonState extends State<StartIGCButton>
|
|||
case AssistanceState.noMessage:
|
||||
case AssistanceState.fetched:
|
||||
case AssistanceState.complete:
|
||||
case AssistanceState.error:
|
||||
return Theme.of(context).colorScheme.surfaceContainerHighest;
|
||||
case AssistanceState.notFetched:
|
||||
case AssistanceState.fetching:
|
||||
|
|
|
|||
29
lib/pangea/common/utils/feedback_model.dart
Normal file
29
lib/pangea/common/utils/feedback_model.dart
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
sealed class FeedbackState<T> {
|
||||
const FeedbackState();
|
||||
}
|
||||
|
||||
class FeedbackIdle<T> extends FeedbackState<T> {}
|
||||
|
||||
class FeedbackLoading<T> extends FeedbackState<T> {}
|
||||
|
||||
class FeedbackLoaded<T> extends FeedbackState<T> {
|
||||
final T value;
|
||||
const FeedbackLoaded(this.value);
|
||||
}
|
||||
|
||||
class FeedbackError<T> extends FeedbackState<T> {
|
||||
final Object error;
|
||||
const FeedbackError(this.error);
|
||||
}
|
||||
|
||||
class FeedbackModel<T> extends ChangeNotifier {
|
||||
FeedbackState<T> _state = FeedbackIdle<T>();
|
||||
FeedbackState<T> get state => _state;
|
||||
|
||||
void setState(FeedbackState<T> newState) {
|
||||
_state = newState;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/card_header.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/overlay.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_toggle.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
/// Instruction Card gives users tips on
|
||||
/// how to use Pangea Chat's features
|
||||
Future<void> instructionsShowPopup(
|
||||
BuildContext context,
|
||||
InstructionsEnum key,
|
||||
String transformTargetKey, {
|
||||
bool showToggle = true,
|
||||
Widget? customContent,
|
||||
bool forceShow = false,
|
||||
}) async {
|
||||
final bool userLangsSet =
|
||||
await MatrixState.pangeaController.userController.areUserLanguagesSet;
|
||||
if (!userLangsSet) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if ((_instructionsShown[key.toString()] ?? false) && !forceShow) {
|
||||
// return;
|
||||
// }
|
||||
// _instructionsShown[key.toString()] = true;
|
||||
|
||||
if (key.isToggledOff && !forceShow) {
|
||||
return;
|
||||
}
|
||||
|
||||
final botStyle = BotStyle.text(context);
|
||||
Future.delayed(
|
||||
const Duration(seconds: 1),
|
||||
() {
|
||||
if (!context.mounted) return;
|
||||
OverlayUtil.showPositionedCard(
|
||||
context: context,
|
||||
backDropToDismiss: false,
|
||||
cardToShow: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
CardHeader(
|
||||
text: key.title(L10n.of(context)),
|
||||
botExpression: BotExpression.idle,
|
||||
// onClose: () => {_instructionsClosed[key.toString()] = true},
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: Text(
|
||||
key.body(L10n.of(context)),
|
||||
style: botStyle,
|
||||
),
|
||||
),
|
||||
if (customContent != null) customContent,
|
||||
if (showToggle) InstructionsToggle(instructionsKey: key),
|
||||
],
|
||||
),
|
||||
maxHeight: 300,
|
||||
maxWidth: 300,
|
||||
transformTargetId: transformTargetKey,
|
||||
closePrevOverlay: false,
|
||||
overlayKey: key.toString(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -55,6 +55,13 @@ class NewCoursePageState extends State<NewCoursePage> {
|
|||
_loadCourses();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_courses.dispose();
|
||||
_targetLanguageFilter.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
CourseFilter get _filter {
|
||||
return CourseFilter(
|
||||
targetLanguage: _targetLanguageFilter.value,
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ class _PhoneticTranscriptionWidgetState
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _handleAudioTap(BuildContext context) async {
|
||||
Future<void> _handleAudioTap() async {
|
||||
if (_isPlaying) {
|
||||
await TtsController.stop();
|
||||
setState(() => _isPlaying = false);
|
||||
|
|
@ -146,7 +146,7 @@ class _PhoneticTranscriptionWidgetState
|
|||
child: HoverBuilder(
|
||||
builder: (context, hovering) {
|
||||
return GestureDetector(
|
||||
onTap: () => _handleAudioTap(context),
|
||||
onTap: _handleAudioTap,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -156,58 +156,66 @@ class _PhoneticTranscriptionWidgetState
|
|||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_error != null)
|
||||
_error is UnsubscribedException
|
||||
? ErrorIndicator(
|
||||
message: L10n.of(context)
|
||||
.subscribeToUnlockTranscriptions,
|
||||
onTap: () {
|
||||
MatrixState
|
||||
.pangeaController.subscriptionController
|
||||
.showPaywall(context);
|
||||
},
|
||||
)
|
||||
: ErrorIndicator(
|
||||
message:
|
||||
L10n.of(context).failedToFetchTranscription,
|
||||
)
|
||||
else if (_isLoading || _transcription == null)
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
)
|
||||
else
|
||||
Flexible(
|
||||
child: Text(
|
||||
_transcription!,
|
||||
textScaler: TextScaler.noScaling,
|
||||
style: widget.style ??
|
||||
Theme.of(context).textTheme.bodyMedium,
|
||||
child: CompositedTransformTarget(
|
||||
link: MatrixState.pAnyState
|
||||
.layerLinkAndKey("phonetic-transcription-${widget.text}")
|
||||
.link,
|
||||
child: Row(
|
||||
key: MatrixState.pAnyState
|
||||
.layerLinkAndKey("phonetic-transcription-${widget.text}")
|
||||
.key,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_error != null)
|
||||
_error is UnsubscribedException
|
||||
? ErrorIndicator(
|
||||
message: L10n.of(context)
|
||||
.subscribeToUnlockTranscriptions,
|
||||
onTap: () {
|
||||
MatrixState
|
||||
.pangeaController.subscriptionController
|
||||
.showPaywall(context);
|
||||
},
|
||||
)
|
||||
: ErrorIndicator(
|
||||
message:
|
||||
L10n.of(context).failedToFetchTranscription,
|
||||
)
|
||||
else if (_isLoading || _transcription == null)
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
)
|
||||
else
|
||||
Flexible(
|
||||
child: Text(
|
||||
_transcription!,
|
||||
textScaler: TextScaler.noScaling,
|
||||
style: widget.style ??
|
||||
Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_transcription != null &&
|
||||
_error == null &&
|
||||
widget.enabled)
|
||||
const SizedBox(width: 8),
|
||||
if (_transcription != null &&
|
||||
_error == null &&
|
||||
widget.enabled)
|
||||
Tooltip(
|
||||
message: _isPlaying
|
||||
? L10n.of(context).stop
|
||||
: L10n.of(context).playAudio,
|
||||
child: Icon(
|
||||
_isPlaying ? Icons.pause_outlined : Icons.volume_up,
|
||||
size: widget.iconSize ?? 24,
|
||||
color: widget.iconColor ??
|
||||
Theme.of(context).iconTheme.color,
|
||||
if (_transcription != null &&
|
||||
_error == null &&
|
||||
widget.enabled)
|
||||
const SizedBox(width: 8),
|
||||
if (_transcription != null &&
|
||||
_error == null &&
|
||||
widget.enabled)
|
||||
Tooltip(
|
||||
message: _isPlaying
|
||||
? L10n.of(context).stop
|
||||
: L10n.of(context).playAudio,
|
||||
child: Icon(
|
||||
_isPlaying ? Icons.pause_outlined : Icons.volume_up,
|
||||
size: widget.iconSize ?? 24,
|
||||
color: widget.iconColor ??
|
||||
Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,11 +11,14 @@ import 'package:just_audio/just_audio.dart';
|
|||
import 'package:matrix/matrix.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/events/audio_player.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/card_header.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/overlay.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_show_popup.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -334,11 +337,27 @@ class TtsController {
|
|||
BuildContext context,
|
||||
String targetID,
|
||||
) async =>
|
||||
instructionsShowPopup(
|
||||
context,
|
||||
InstructionsEnum.ttsDisabled,
|
||||
targetID,
|
||||
showToggle: false,
|
||||
forceShow: true,
|
||||
OverlayUtil.showPositionedCard(
|
||||
context: context,
|
||||
backDropToDismiss: false,
|
||||
cardToShow: Column(
|
||||
spacing: 12.0,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
CardHeader(InstructionsEnum.ttsDisabled.title(L10n.of(context))),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: Text(
|
||||
InstructionsEnum.ttsDisabled.body(L10n.of(context)),
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
maxHeight: 300,
|
||||
maxWidth: 300,
|
||||
transformTargetId: targetID,
|
||||
closePrevOverlay: false,
|
||||
overlayKey: InstructionsEnum.ttsDisabled.toString(),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ enum MessageMode {
|
|||
messageMeaning,
|
||||
listening,
|
||||
messageSpeechToText,
|
||||
messageTranslation,
|
||||
|
||||
// message not selected
|
||||
noneSelected,
|
||||
|
|
@ -34,8 +33,6 @@ enum MessageMode {
|
|||
extension MessageModeExtension on MessageMode {
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
case MessageMode.messageTranslation:
|
||||
return Icons.translate;
|
||||
case MessageMode.listening:
|
||||
return Icons.volume_up;
|
||||
case MessageMode.messageSpeechToText:
|
||||
|
|
@ -58,8 +55,6 @@ extension MessageModeExtension on MessageMode {
|
|||
|
||||
String title(BuildContext context) {
|
||||
switch (this) {
|
||||
case MessageMode.messageTranslation:
|
||||
return L10n.of(context).translations;
|
||||
case MessageMode.listening:
|
||||
return L10n.of(context).messageAudio;
|
||||
case MessageMode.messageSpeechToText:
|
||||
|
|
@ -83,8 +78,6 @@ extension MessageModeExtension on MessageMode {
|
|||
|
||||
String tooltip(BuildContext context) {
|
||||
switch (this) {
|
||||
case MessageMode.messageTranslation:
|
||||
return L10n.of(context).translationTooltip;
|
||||
case MessageMode.listening:
|
||||
return L10n.of(context).listen;
|
||||
case MessageMode.messageSpeechToText:
|
||||
|
|
@ -121,8 +114,6 @@ extension MessageModeExtension on MessageMode {
|
|||
return InstructionsEnum.chooseEmoji;
|
||||
case MessageMode.noneSelected:
|
||||
return InstructionsEnum.readingAssistanceOverview;
|
||||
case MessageMode.messageTranslation:
|
||||
return InstructionsEnum.completeActivitiesToUnlock;
|
||||
case MessageMode.messageMeaning:
|
||||
case MessageMode.wordZoom:
|
||||
case MessageMode.practiceActivity:
|
||||
|
|
@ -142,7 +133,6 @@ extension MessageModeExtension on MessageMode {
|
|||
return 0.5;
|
||||
case MessageMode.listening:
|
||||
return 0.3;
|
||||
case MessageMode.messageTranslation:
|
||||
case MessageMode.messageSpeechToText:
|
||||
case MessageMode.wordZoom:
|
||||
case MessageMode.wordEmoji:
|
||||
|
|
@ -156,8 +146,6 @@ extension MessageModeExtension on MessageMode {
|
|||
MessageOverlayController overlayController,
|
||||
) {
|
||||
switch (this) {
|
||||
case MessageMode.messageTranslation:
|
||||
return overlayController.isTranslationUnlocked;
|
||||
case MessageMode.practiceActivity:
|
||||
case MessageMode.listening:
|
||||
case MessageMode.messageSpeechToText:
|
||||
|
|
@ -175,8 +163,6 @@ extension MessageModeExtension on MessageMode {
|
|||
|
||||
bool isModeDone(MessageOverlayController overlayController) {
|
||||
switch (this) {
|
||||
case MessageMode.messageTranslation:
|
||||
return overlayController.isTotallyDone;
|
||||
case MessageMode.listening:
|
||||
return overlayController.isListeningDone;
|
||||
case MessageMode.wordEmoji:
|
||||
|
|
@ -230,7 +216,6 @@ extension MessageModeExtension on MessageMode {
|
|||
|
||||
case MessageMode.noneSelected:
|
||||
case MessageMode.messageMeaning:
|
||||
case MessageMode.messageTranslation:
|
||||
case MessageMode.wordZoom:
|
||||
case MessageMode.messageSpeechToText:
|
||||
case MessageMode.practiceActivity:
|
||||
|
|
@ -290,7 +275,6 @@ extension MessageModeExtension on MessageMode {
|
|||
|
||||
case MessageMode.noneSelected:
|
||||
case MessageMode.messageMeaning:
|
||||
case MessageMode.messageTranslation:
|
||||
case MessageMode.wordZoom:
|
||||
case MessageMode.messageSpeechToText:
|
||||
case MessageMode.practiceActivity:
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_mode_locked_card.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_translation_card.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_mode_buttons.dart';
|
||||
|
||||
|
|
@ -73,15 +71,6 @@ class ReadingAssistanceInputBarState extends State<ReadingAssistanceInputBar> {
|
|||
textAlign: TextAlign.center,
|
||||
);
|
||||
|
||||
case MessageMode.messageTranslation:
|
||||
if (overlayController.isTranslationUnlocked) {
|
||||
content = MessageTranslationCard(
|
||||
messageEvent: overlayController.pangeaMessageEvent,
|
||||
);
|
||||
} else {
|
||||
content = MessageModeLockedCard(controller: overlayController);
|
||||
}
|
||||
|
||||
case MessageMode.wordEmoji:
|
||||
case MessageMode.wordMeaning:
|
||||
case MessageMode.listening:
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/toolbar_button.dart';
|
||||
|
||||
class MessageMeaningButton extends StatelessWidget {
|
||||
final MessageOverlayController overlayController;
|
||||
final double buttonSize;
|
||||
|
||||
const MessageMeaningButton({
|
||||
super.key,
|
||||
required this.overlayController,
|
||||
required this.buttonSize,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedCrossFade(
|
||||
crossFadeState: overlayController.isPracticeComplete
|
||||
? CrossFadeState.showSecond
|
||||
: CrossFadeState.showFirst,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
firstChild: ToolbarButton(
|
||||
mode: MessageMode.messageMeaning,
|
||||
overlayController: overlayController,
|
||||
buttonSize: buttonSize,
|
||||
),
|
||||
secondChild: Container(
|
||||
width: buttonSize,
|
||||
height: buttonSize,
|
||||
alignment: Alignment.center,
|
||||
child: Icon(
|
||||
MessageMode.messageMeaning.icon,
|
||||
color: AppConfig.gold,
|
||||
size: buttonSize,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
|
||||
class MessageModeLockedCard extends StatelessWidget {
|
||||
final MessageOverlayController controller;
|
||||
|
||||
const MessageModeLockedCard({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.lock_outline,
|
||||
size: 40,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
// if (!InstructionsEnum.completeActivitiesToUnlock.isToggledOff) ...[
|
||||
// const SizedBox(height: 8),
|
||||
// const InstructionsInlineTooltip(
|
||||
// instructionsEnum: InstructionsEnum.completeActivitiesToUnlock,
|
||||
// bold: true,
|
||||
// ),
|
||||
// ],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -337,8 +337,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
bool get hideWordCardContent =>
|
||||
readingAssistanceMode == ReadingAssistanceMode.practiceMode;
|
||||
|
||||
bool get isPracticeComplete => isTranslationUnlocked;
|
||||
|
||||
bool isPracticeActivityDone(ActivityTypeEnum activityType) =>
|
||||
practiceSelection?.activities(activityType).every((a) => a.isComplete) ==
|
||||
true;
|
||||
|
|
@ -353,15 +351,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
|
||||
bool get isMorphDone => isPracticeActivityDone(ActivityTypeEnum.morphId);
|
||||
|
||||
/// you have to complete one of the mode mini-games to unlock translation
|
||||
bool get isTranslationUnlocked =>
|
||||
pangeaMessageEvent.ownMessage == true ||
|
||||
!messageInUserL2 ||
|
||||
isEmojiDone ||
|
||||
isMeaningDone ||
|
||||
isListeningDone ||
|
||||
isMorphDone;
|
||||
|
||||
bool get isTotallyDone =>
|
||||
isEmojiDone && isMeaningDone && isListeningDone && isMorphDone;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,103 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/card_error_widget.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/toolbar_content_loading_indicator.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class MessageTranslationCard extends StatefulWidget {
|
||||
final PangeaMessageEvent messageEvent;
|
||||
|
||||
const MessageTranslationCard({
|
||||
super.key,
|
||||
required this.messageEvent,
|
||||
});
|
||||
|
||||
@override
|
||||
MessageTranslationCardState createState() => MessageTranslationCardState();
|
||||
}
|
||||
|
||||
class MessageTranslationCardState extends State<MessageTranslationCard> {
|
||||
PangeaRepresentation? repEvent;
|
||||
bool _fetchingTranslation = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
loadTranslation();
|
||||
}
|
||||
|
||||
Future<void> loadTranslation() async {
|
||||
if (!mounted) return;
|
||||
try {
|
||||
setState(() => _fetchingTranslation = true);
|
||||
repEvent = await widget.messageEvent.l1Respresentation();
|
||||
} catch (err) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
data: {},
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _fetchingTranslation = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String? get l1Code =>
|
||||
MatrixState.pangeaController.languageController.activeL1Code();
|
||||
String? get l2Code =>
|
||||
MatrixState.pangeaController.languageController.activeL2Code();
|
||||
|
||||
/// Show warning if message's language code is user's L1
|
||||
/// or if translated text is same as original text.
|
||||
/// Warning does not show if was previously closed
|
||||
bool get notGoingToTranslate {
|
||||
final bool isWrittenInL1 =
|
||||
l1Code != null && widget.messageEvent.originalSent?.langCode == l1Code;
|
||||
|
||||
return isWrittenInL1;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint('MessageTranslationCard build');
|
||||
if (!_fetchingTranslation && repEvent == null) {
|
||||
return CardErrorWidget(
|
||||
error: L10n.of(context).errorFetchingTranslation,
|
||||
maxWidth: AppConfig.toolbarMinWidth,
|
||||
);
|
||||
}
|
||||
|
||||
final loadingTranslation = repEvent == null;
|
||||
|
||||
if (_fetchingTranslation || loadingTranslation) {
|
||||
return const ToolbarContentLoadingIndicator();
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
repEvent!.text,
|
||||
style: AppConfig.messageTextStyle(
|
||||
widget.messageEvent.event,
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (notGoingToTranslate)
|
||||
const InstructionsInlineTooltip(
|
||||
instructionsEnum: InstructionsEnum.l1Translation,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -237,7 +237,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
onPressed: updateChoice,
|
||||
selectedChoiceIndex: selectedChoiceIndex,
|
||||
choices: choices(context),
|
||||
isActive: true,
|
||||
enabled: true,
|
||||
id: currentRecordModel?.hashCode.toString(),
|
||||
enableAudio: practiceActivity.activityType.includeTTSOnClick,
|
||||
langCode:
|
||||
|
|
|
|||
|
|
@ -296,10 +296,7 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
Widget build(BuildContext context) {
|
||||
if (_error != null || (!fetchingActivity && currentActivity == null)) {
|
||||
debugger(when: kDebugMode);
|
||||
return CardErrorWidget(
|
||||
error: L10n.of(context).errorFetchingActivity,
|
||||
maxWidth: 500,
|
||||
);
|
||||
return CardErrorWidget(L10n.of(context).errorFetchingActivity);
|
||||
}
|
||||
|
||||
return Column(
|
||||
|
|
|
|||
|
|
@ -42,11 +42,6 @@ class WordZoomActivityButton extends StatelessWidget {
|
|||
iconSize: 24, // Keep this constant as scaling handles the size change
|
||||
color: isSelected ? Theme.of(context).colorScheme.primary : null,
|
||||
visualDensity: VisualDensity.compact,
|
||||
// style: IconButton.styleFrom(
|
||||
// backgroundColor: isSelected
|
||||
// ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.25)
|
||||
// : Colors.transparent,
|
||||
// ),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,10 +22,6 @@ class ToolbarButton extends StatelessWidget {
|
|||
overlayController,
|
||||
);
|
||||
|
||||
bool get enabled => mode == MessageMode.messageTranslation
|
||||
? overlayController.isTranslationUnlocked
|
||||
: true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue