Open AI Scribe from text selection
This commit is contained in:
@@ -86,6 +86,7 @@ import 'package:tmail_ui_user/features/composer/presentation/model/saved_composi
|
|||||||
import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/styles/composer_style.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/styles/composer_style.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/view/editor_view_mixin.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/view/editor_view_mixin.dart';
|
||||||
|
import 'package:tmail_ui_user/features/composer/presentation/widgets/mixins/text_selection_mixin.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from_composer_bottom_sheet_builder.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from_composer_bottom_sheet_builder.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/widgets/saving_message_dialog_view.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/widgets/saving_message_dialog_view.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/widgets/saving_template_dialog_view.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/widgets/saving_template_dialog_view.dart';
|
||||||
@@ -158,6 +159,10 @@ class ComposerController extends BaseController
|
|||||||
final isMarkAsImportant = Rx<bool>(false);
|
final isMarkAsImportant = Rx<bool>(false);
|
||||||
final isContentHeightExceeded = Rx<bool>(false);
|
final isContentHeightExceeded = Rx<bool>(false);
|
||||||
|
|
||||||
|
final selectedText = Rxn<String>();
|
||||||
|
final hasTextSelection = false.obs;
|
||||||
|
final textSelectionCoordinates = Rxn<Offset>();
|
||||||
|
|
||||||
final LocalFilePickerInteractor _localFilePickerInteractor;
|
final LocalFilePickerInteractor _localFilePickerInteractor;
|
||||||
final LocalImagePickerInteractor _localImagePickerInteractor;
|
final LocalImagePickerInteractor _localImagePickerInteractor;
|
||||||
final GetEmailContentInteractor _getEmailContentInteractor;
|
final GetEmailContentInteractor _getEmailContentInteractor;
|
||||||
@@ -913,6 +918,43 @@ class ComposerController extends BaseController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void showAIScribeMenuForSelectedText(BuildContext context, {Offset? buttonPosition}) {
|
||||||
|
final selection = selectedText.value;
|
||||||
|
if (selection == null || selection.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showAIScribeDialog(
|
||||||
|
context: context,
|
||||||
|
imagePaths: imagePaths,
|
||||||
|
content: selection,
|
||||||
|
onInsertText: insertTextInEditor,
|
||||||
|
buttonPosition: buttonPosition,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleTextSelection(TextSelectionData? textSelectionData) {
|
||||||
|
if (textSelectionData != null) {
|
||||||
|
hasTextSelection.value = textSelectionData.hasSelection;
|
||||||
|
selectedText.value = textSelectionData.selectedText;
|
||||||
|
|
||||||
|
if (textSelectionData.coordinates != null) {
|
||||||
|
textSelectionCoordinates.value = Offset(
|
||||||
|
textSelectionData.coordinates!.x,
|
||||||
|
textSelectionData.coordinates!.y + 6,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
textSelectionCoordinates.value = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hasTextSelection.value = false;
|
||||||
|
selectedText.value = null;
|
||||||
|
textSelectionCoordinates.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _prepareToSendMessages(BuildContext context) async {
|
Future<void> _prepareToSendMessages(BuildContext context) async {
|
||||||
final arguments = composerArguments.value;
|
final arguments = composerArguments.value;
|
||||||
final session = mailboxDashBoardController.sessionCurrent;
|
final session = mailboxDashBoardController.sessionCurrent;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:flutter_svg/flutter_svg.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
|
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
|
||||||
import 'package:model/email/prefix_email_address.dart';
|
import 'package:model/email/prefix_email_address.dart';
|
||||||
|
import 'package:scribe/scribe/ai/presentation/widgets/ai_scribe_button.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_content_height_exceeded_extension.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_content_height_exceeded_extension.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_edit_recipient_extension.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_edit_recipient_extension.dart';
|
||||||
@@ -235,28 +236,38 @@ class ComposerView extends GetWidget<ComposerController> {
|
|||||||
))
|
))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Obx(() => Padding(
|
Stack(
|
||||||
padding: ComposerStyle.mobileEditorPadding,
|
children: [
|
||||||
child: MobileEditorView(
|
Column(
|
||||||
arguments: controller.composerArguments.value,
|
children: [
|
||||||
contentViewState: controller.emailContentsViewState.value,
|
Obx(() => Padding(
|
||||||
onCreatedEditorAction: controller.onCreatedMobileEditorAction,
|
padding: ComposerStyle.mobileEditorPadding,
|
||||||
onLoadCompletedEditorAction: controller.onLoadCompletedMobileEditorAction,
|
child: MobileEditorView(
|
||||||
onEditorContentHeightChanged: controller.onEditorContentHeightChangedOnIOS,
|
arguments: controller.composerArguments.value,
|
||||||
),
|
contentViewState: controller.emailContentsViewState.value,
|
||||||
)),
|
onCreatedEditorAction: controller.onCreatedMobileEditorAction,
|
||||||
Obx(() {
|
onLoadCompletedEditorAction: controller.onLoadCompletedMobileEditorAction,
|
||||||
if (controller.isContentHeightExceeded.isTrue && PlatformInfo.isIOS) {
|
onEditorContentHeightChanged: controller.onEditorContentHeightChangedOnIOS,
|
||||||
return ViewEntireMessageWithMessageClippedWidget(
|
onTextSelectionChanged: controller.handleTextSelection,
|
||||||
buttonActionName: AppLocalizations.of(context).viewEntireMessage.toUpperCase(),
|
),
|
||||||
onViewEntireMessageAction: controller.viewEntireContent,
|
)),
|
||||||
topPadding: 12,
|
Obx(() {
|
||||||
);
|
if (controller.isContentHeightExceeded.isTrue && PlatformInfo.isIOS) {
|
||||||
} else {
|
return ViewEntireMessageWithMessageClippedWidget(
|
||||||
return const SizedBox.shrink();
|
buttonActionName: AppLocalizations.of(context).viewEntireMessage.toUpperCase(),
|
||||||
}
|
onViewEntireMessageAction: controller.viewEntireContent,
|
||||||
}),
|
topPadding: 12,
|
||||||
SizedBox(height: MediaQuery.viewInsetsOf(context).bottom + 64),
|
);
|
||||||
|
} else {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
SizedBox(height: MediaQuery.viewInsetsOf(context).bottom + 64),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
_buildAIScribeSelectionButton(context),
|
||||||
|
],
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -414,28 +425,38 @@ class ComposerView extends GetWidget<ComposerController> {
|
|||||||
margin: ComposerStyle.insertImageLoadingBarMargin,
|
margin: ComposerStyle.insertImageLoadingBarMargin,
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
Obx(() => Padding(
|
Stack(
|
||||||
padding: ComposerStyle.mobileEditorPadding,
|
children: [
|
||||||
child: MobileEditorView(
|
Column(
|
||||||
arguments: controller.composerArguments.value,
|
children: [
|
||||||
contentViewState: controller.emailContentsViewState.value,
|
Obx(() => Padding(
|
||||||
onCreatedEditorAction: controller.onCreatedMobileEditorAction,
|
padding: ComposerStyle.mobileEditorPadding,
|
||||||
onLoadCompletedEditorAction: controller.onLoadCompletedMobileEditorAction,
|
child: MobileEditorView(
|
||||||
onEditorContentHeightChanged: controller.onEditorContentHeightChangedOnIOS,
|
arguments: controller.composerArguments.value,
|
||||||
),
|
contentViewState: controller.emailContentsViewState.value,
|
||||||
)),
|
onCreatedEditorAction: controller.onCreatedMobileEditorAction,
|
||||||
Obx(() {
|
onLoadCompletedEditorAction: controller.onLoadCompletedMobileEditorAction,
|
||||||
if (controller.isContentHeightExceeded.isTrue && PlatformInfo.isIOS) {
|
onEditorContentHeightChanged: controller.onEditorContentHeightChangedOnIOS,
|
||||||
return ViewEntireMessageWithMessageClippedWidget(
|
onTextSelectionChanged: controller.handleTextSelection,
|
||||||
buttonActionName: AppLocalizations.of(context).viewEntireMessage.toUpperCase(),
|
),
|
||||||
onViewEntireMessageAction: controller.viewEntireContent,
|
)),
|
||||||
topPadding: 12,
|
Obx(() {
|
||||||
);
|
if (controller.isContentHeightExceeded.isTrue && PlatformInfo.isIOS) {
|
||||||
} else {
|
return ViewEntireMessageWithMessageClippedWidget(
|
||||||
return const SizedBox.shrink();
|
buttonActionName: AppLocalizations.of(context).viewEntireMessage.toUpperCase(),
|
||||||
}
|
onViewEntireMessageAction: controller.viewEntireContent,
|
||||||
}),
|
topPadding: 12,
|
||||||
SizedBox(height: MediaQuery.viewInsetsOf(context).bottom + 64),
|
);
|
||||||
|
} else {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
SizedBox(height: MediaQuery.viewInsetsOf(context).bottom + 64),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
_buildAIScribeSelectionButton(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -533,4 +554,40 @@ class ComposerView extends GetWidget<ComposerController> {
|
|||||||
onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputOnMobileAction,
|
onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputOnMobileAction,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildAIScribeSelectionButton(BuildContext context) {
|
||||||
|
return Obx(() {
|
||||||
|
if (controller.hasTextSelection.value &&
|
||||||
|
controller.textSelectionCoordinates.value != null) {
|
||||||
|
final coordinates = controller.textSelectionCoordinates.value!;
|
||||||
|
// Account for the horizontal padding around the editor
|
||||||
|
const editorHorizontalPadding = 12.0;
|
||||||
|
return PositionedDirectional(
|
||||||
|
start: coordinates.dx + editorHorizontalPadding,
|
||||||
|
top: coordinates.dy,
|
||||||
|
child: Builder(
|
||||||
|
builder: (buttonContext) {
|
||||||
|
return AIScribeButton(
|
||||||
|
imagePaths: controller.imagePaths,
|
||||||
|
onTap: () {
|
||||||
|
final RenderBox? renderBox = buttonContext.findRenderObject() as RenderBox?;
|
||||||
|
if (renderBox != null) {
|
||||||
|
final position = renderBox.localToGlobal(Offset.zero);
|
||||||
|
controller.showAIScribeMenuForSelectedText(
|
||||||
|
context,
|
||||||
|
buttonPosition: position,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
controller.showAIScribeMenuForSelectedText(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import 'package:get/get.dart';
|
|||||||
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
|
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
|
||||||
import 'package:model/email/prefix_email_address.dart';
|
import 'package:model/email/prefix_email_address.dart';
|
||||||
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
||||||
|
import 'package:scribe/scribe/ai/presentation/widgets/ai_scribe_button.dart';
|
||||||
import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_manager.dart';
|
import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_manager.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/dialog_picker/color_dialog_picker.dart';
|
import 'package:tmail_ui_user/features/base/widget/dialog_picker/color_dialog_picker.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/keyboard/keyboard_handler_wrapper.dart';
|
import 'package:tmail_ui_user/features/base/widget/keyboard/keyboard_handler_wrapper.dart';
|
||||||
@@ -267,6 +268,7 @@ class ComposerView extends GetWidget<ComposerController> {
|
|||||||
),
|
),
|
||||||
onInitialContentLoadComplete: controller.onInitialContentLoadCompleteWeb,
|
onInitialContentLoadComplete: controller.onInitialContentLoadCompleteWeb,
|
||||||
onKeyDownEditorAction: controller.onKeyDownEditorAction,
|
onKeyDownEditorAction: controller.onKeyDownEditorAction,
|
||||||
|
onTextSelectionChanged: controller.handleTextSelection,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -332,6 +334,7 @@ class ComposerView extends GetWidget<ComposerController> {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
_buildAIScribeSelectionButton(context),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -509,6 +512,7 @@ class ComposerView extends GetWidget<ComposerController> {
|
|||||||
),
|
),
|
||||||
onInitialContentLoadComplete: controller.onInitialContentLoadCompleteWeb,
|
onInitialContentLoadComplete: controller.onInitialContentLoadCompleteWeb,
|
||||||
onKeyDownEditorAction: controller.onKeyDownEditorAction,
|
onKeyDownEditorAction: controller.onKeyDownEditorAction,
|
||||||
|
onTextSelectionChanged: controller.handleTextSelection,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -604,6 +608,7 @@ class ComposerView extends GetWidget<ComposerController> {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
_buildAIScribeSelectionButton(context),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -784,6 +789,7 @@ class ComposerView extends GetWidget<ComposerController> {
|
|||||||
),
|
),
|
||||||
onInitialContentLoadComplete: controller.onInitialContentLoadCompleteWeb,
|
onInitialContentLoadComplete: controller.onInitialContentLoadCompleteWeb,
|
||||||
onKeyDownEditorAction: controller.onKeyDownEditorAction,
|
onKeyDownEditorAction: controller.onKeyDownEditorAction,
|
||||||
|
onTextSelectionChanged: controller.handleTextSelection,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -877,6 +883,7 @@ class ComposerView extends GetWidget<ComposerController> {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
_buildAIScribeSelectionButton(context),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -949,4 +956,40 @@ class ComposerView extends GetWidget<ComposerController> {
|
|||||||
onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputOnMobileAction,
|
onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputOnMobileAction,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildAIScribeSelectionButton(BuildContext context) {
|
||||||
|
return Obx(() {
|
||||||
|
if (controller.hasTextSelection.value &&
|
||||||
|
controller.textSelectionCoordinates.value != null) {
|
||||||
|
final coordinates = controller.textSelectionCoordinates.value!;
|
||||||
|
return PositionedDirectional(
|
||||||
|
start: coordinates.dx,
|
||||||
|
top: coordinates.dy,
|
||||||
|
child: PointerInterceptor(
|
||||||
|
child: Builder(
|
||||||
|
builder: (buttonContext) {
|
||||||
|
return AIScribeButton(
|
||||||
|
imagePaths: controller.imagePaths,
|
||||||
|
onTap: () {
|
||||||
|
final RenderBox? renderBox = buttonContext.findRenderObject() as RenderBox?;
|
||||||
|
if (renderBox != null) {
|
||||||
|
final position = renderBox.localToGlobal(Offset.zero);
|
||||||
|
controller.showAIScribeMenuForSelectedText(
|
||||||
|
context,
|
||||||
|
buttonPosition: position,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
controller.showAIScribeMenuForSelectedText(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user