widgets refactor

This commit is contained in:
ggurdin 2025-11-03 12:52:22 -05:00
parent 9ce99daac9
commit 2b522b6dd7
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
40 changed files with 873 additions and 1669 deletions

View file

@ -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."
}

View file

@ -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),

View file

@ -1 +0,0 @@

View file

@ -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();
}

View file

@ -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();
},
);
}

View file

@ -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),
],
);
}
}

View file

@ -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,

View file

@ -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,

View file

@ -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) {

View file

@ -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);

View file

@ -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;
}
}
}

View file

@ -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,
);
}
}

View file

@ -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(

View file

@ -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,
),

View file

@ -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,
),
],
);
}
}

View file

@ -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),
),

View file

@ -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,
);
},
);

View file

@ -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),
),
],
),
),
],
),
],
);

View file

@ -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,
),
),
],
),
);
}
}

View file

@ -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)),
};
},
),
),
),

View file

@ -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,
),
),
],
),
);
},
);
}
}

View file

@ -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),
),
),
),
],
);
}
}

View file

@ -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,
),
);
}
}

View file

@ -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,
),
);
},
);

View file

@ -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:

View 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();
}
}

View file

@ -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(),
);
},
);
}

View file

@ -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,

View file

@ -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,
),
),
),
],
],
),
),
),
);

View file

@ -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(),
);
}

View file

@ -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:

View file

@ -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:

View file

@ -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,
),
),
);
}
}

View file

@ -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,
// ),
// ],
],
);
}
}

View file

@ -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;

View file

@ -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,
),
],
);
}
}

View file

@ -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:

View file

@ -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(

View file

@ -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,
// ),
),
),
);

View file

@ -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(