mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge branch 'main' into main
This commit is contained in:
commit
9213c233ff
11
CHANGELOG.md
11
CHANGELOG.md
@ -1,4 +1,15 @@
|
|||||||
# Release Notes
|
# Release Notes
|
||||||
|
## Version 0.6.7 - 13/08/2024
|
||||||
|
### New Features
|
||||||
|
- Redesigned the icon picker design on Desktop.
|
||||||
|
- Redesigned the notification page on Mobile.
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- Enhance the toolbar tooltip functionality on Desktop.
|
||||||
|
- Enhance the slash menu user experience on Desktop.
|
||||||
|
- Fixed the issue where list style overrides occurred during text pasting.
|
||||||
|
- Fixed the issue where linking multiple databases in the same document could cause random loss of focus.
|
||||||
|
|
||||||
## Version 0.6.6 - 30/07/2024
|
## Version 0.6.6 - 30/07/2024
|
||||||
### New Features
|
### New Features
|
||||||
- Upgrade your workspace to a premium plan to unlock more features and storage.
|
- Upgrade your workspace to a premium plan to unlock more features and storage.
|
||||||
|
0
frontend/appflowy_flutter/build.yaml
Normal file
0
frontend/appflowy_flutter/build.yaml
Normal file
@ -1,10 +1,10 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
@ -25,6 +25,7 @@ void main() {
|
|||||||
const lines = 3;
|
const lines = 3;
|
||||||
final text = List.generate(lines, (index) => 'line $index').join('\n');
|
final text = List.generate(lines, (index) => 'line $index').join('\n');
|
||||||
AppFlowyClipboard.mockSetData(AppFlowyClipboardData(text: text));
|
AppFlowyClipboard.mockSetData(AppFlowyClipboardData(text: text));
|
||||||
|
ClipboardService.mockSetData(ClipboardServiceData(plainText: text));
|
||||||
|
|
||||||
await insertCodeBlockInDocument(tester);
|
await insertCodeBlockInDocument(tester);
|
||||||
|
|
||||||
@ -51,7 +52,9 @@ Future<void> insertCodeBlockInDocument(WidgetTester tester) async {
|
|||||||
// open the actions menu and insert the codeBlock
|
// open the actions menu and insert the codeBlock
|
||||||
await tester.editor.showSlashMenu();
|
await tester.editor.showSlashMenu();
|
||||||
await tester.editor.tapSlashMenuItemWithName(
|
await tester.editor.tapSlashMenuItemWithName(
|
||||||
LocaleKeys.document_selectionMenu_codeBlock.tr(),
|
LocaleKeys.document_slashMenu_name_code.tr(),
|
||||||
|
offset: 150,
|
||||||
);
|
);
|
||||||
|
// wait for the codeBlock to be inserted
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
@ -165,6 +165,44 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('paste text on part of bullet list', (tester) async {
|
||||||
|
const plainText = 'test';
|
||||||
|
|
||||||
|
await tester.pasteContent(
|
||||||
|
plainText: plainText,
|
||||||
|
beforeTest: (editorState) async {
|
||||||
|
final transaction = editorState.transaction;
|
||||||
|
transaction.insertNodes(
|
||||||
|
[0],
|
||||||
|
[
|
||||||
|
Node(
|
||||||
|
type: BulletedListBlockKeys.type,
|
||||||
|
attributes: {
|
||||||
|
'delta': [
|
||||||
|
{"insert": "bullet list"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set the selection to the second numbered list node (which has empty delta)
|
||||||
|
transaction.afterSelection = Selection(
|
||||||
|
start: Position(path: [0], offset: 7),
|
||||||
|
end: Position(path: [0], offset: 11),
|
||||||
|
);
|
||||||
|
|
||||||
|
await editorState.apply(transaction);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
},
|
||||||
|
(editorState) {
|
||||||
|
final node = editorState.getNodeAtPath([0]);
|
||||||
|
expect(node?.delta?.toPlainText(), 'bullet test');
|
||||||
|
expect(node?.type, BulletedListBlockKeys.type);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('paste image(png) from memory', (tester) async {
|
testWidgets('paste image(png) from memory', (tester) async {
|
||||||
final image = await rootBundle.load('assets/test/images/sample.png');
|
final image = await rootBundle.load('assets/test/images/sample.png');
|
||||||
final bytes = image.buffer.asUint8List();
|
final bytes = image.buffer.asUint8List();
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
|
import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
|
||||||
import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';
|
import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';
|
||||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||||
@ -7,7 +6,6 @@ import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.d
|
|||||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flowy_infra/uuid.dart';
|
import 'package:flowy_infra/uuid.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
@ -22,7 +20,7 @@ void main() {
|
|||||||
await tester.initializeAppFlowy();
|
await tester.initializeAppFlowy();
|
||||||
await tester.tapAnonymousSignInButton();
|
await tester.tapAnonymousSignInButton();
|
||||||
|
|
||||||
await insertReferenceDatabase(tester, ViewLayoutPB.Grid);
|
await insertLinkedDatabase(tester, ViewLayoutPB.Grid);
|
||||||
|
|
||||||
// validate the referenced grid is inserted
|
// validate the referenced grid is inserted
|
||||||
expect(
|
expect(
|
||||||
@ -50,7 +48,7 @@ void main() {
|
|||||||
await tester.initializeAppFlowy();
|
await tester.initializeAppFlowy();
|
||||||
await tester.tapAnonymousSignInButton();
|
await tester.tapAnonymousSignInButton();
|
||||||
|
|
||||||
await insertReferenceDatabase(tester, ViewLayoutPB.Board);
|
await insertLinkedDatabase(tester, ViewLayoutPB.Board);
|
||||||
|
|
||||||
// validate the referenced board is inserted
|
// validate the referenced board is inserted
|
||||||
expect(
|
expect(
|
||||||
@ -66,7 +64,7 @@ void main() {
|
|||||||
await tester.initializeAppFlowy();
|
await tester.initializeAppFlowy();
|
||||||
await tester.tapAnonymousSignInButton();
|
await tester.tapAnonymousSignInButton();
|
||||||
|
|
||||||
await insertReferenceDatabase(tester, ViewLayoutPB.Calendar);
|
await insertLinkedDatabase(tester, ViewLayoutPB.Calendar);
|
||||||
|
|
||||||
// validate the referenced grid is inserted
|
// validate the referenced grid is inserted
|
||||||
expect(
|
expect(
|
||||||
@ -129,7 +127,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a referenced database of [layout] into the document
|
/// Insert a referenced database of [layout] into the document
|
||||||
Future<void> insertReferenceDatabase(
|
Future<void> insertLinkedDatabase(
|
||||||
WidgetTester tester,
|
WidgetTester tester,
|
||||||
ViewLayoutPB layout,
|
ViewLayoutPB layout,
|
||||||
) async {
|
) async {
|
||||||
@ -150,7 +148,7 @@ Future<void> insertReferenceDatabase(
|
|||||||
// insert a referenced view
|
// insert a referenced view
|
||||||
await tester.editor.showSlashMenu();
|
await tester.editor.showSlashMenu();
|
||||||
await tester.editor.tapSlashMenuItemWithName(
|
await tester.editor.tapSlashMenuItemWithName(
|
||||||
layout.referencedMenuName,
|
layout.slashMenuLinkedName,
|
||||||
);
|
);
|
||||||
|
|
||||||
final linkToPageMenu = find.byType(InlineActionsHandler);
|
final linkToPageMenu = find.byType(InlineActionsHandler);
|
||||||
@ -176,16 +174,8 @@ Future<void> createInlineDatabase(
|
|||||||
await tester.editor.tapLineOfEditorAt(0);
|
await tester.editor.tapLineOfEditorAt(0);
|
||||||
// insert a referenced view
|
// insert a referenced view
|
||||||
await tester.editor.showSlashMenu();
|
await tester.editor.showSlashMenu();
|
||||||
final name = switch (layout) {
|
|
||||||
ViewLayoutPB.Grid => LocaleKeys.document_slashMenu_grid_createANewGrid.tr(),
|
|
||||||
ViewLayoutPB.Board =>
|
|
||||||
LocaleKeys.document_slashMenu_board_createANewBoard.tr(),
|
|
||||||
ViewLayoutPB.Calendar =>
|
|
||||||
LocaleKeys.document_slashMenu_calendar_createANewCalendar.tr(),
|
|
||||||
_ => '',
|
|
||||||
};
|
|
||||||
await tester.editor.tapSlashMenuItemWithName(
|
await tester.editor.tapSlashMenuItemWithName(
|
||||||
name,
|
layout.slashMenuName,
|
||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/core/config/kv.dart';
|
import 'package:appflowy/core/config/kv.dart';
|
||||||
import 'package:appflowy/core/config/kv_keys.dart';
|
import 'package:appflowy/core/config/kv_keys.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
@ -10,6 +8,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da
|
|||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
@ -33,7 +32,9 @@ void main() {
|
|||||||
// tap the first line of the document
|
// tap the first line of the document
|
||||||
await tester.editor.tapLineOfEditorAt(0);
|
await tester.editor.tapLineOfEditorAt(0);
|
||||||
await tester.editor.showSlashMenu();
|
await tester.editor.showSlashMenu();
|
||||||
await tester.editor.tapSlashMenuItemWithName('File');
|
await tester.editor.tapSlashMenuItemWithName(
|
||||||
|
LocaleKeys.document_slashMenu_name_file.tr(),
|
||||||
|
);
|
||||||
expect(find.byType(FileBlockComponent), findsOneWidget);
|
expect(find.byType(FileBlockComponent), findsOneWidget);
|
||||||
|
|
||||||
await tester.tap(find.byType(FileBlockComponent));
|
await tester.tap(find.byType(FileBlockComponent));
|
||||||
@ -111,7 +112,9 @@ void main() {
|
|||||||
// tap the first line of the document
|
// tap the first line of the document
|
||||||
await tester.editor.tapLineOfEditorAt(0);
|
await tester.editor.tapLineOfEditorAt(0);
|
||||||
await tester.editor.showSlashMenu();
|
await tester.editor.showSlashMenu();
|
||||||
await tester.editor.tapSlashMenuItemWithName('File');
|
await tester.editor.tapSlashMenuItemWithName(
|
||||||
|
LocaleKeys.document_slashMenu_name_file.tr(),
|
||||||
|
);
|
||||||
expect(find.byType(FileBlockComponent), findsOneWidget);
|
expect(find.byType(FileBlockComponent), findsOneWidget);
|
||||||
|
|
||||||
await tester.tap(find.byType(FileBlockComponent));
|
await tester.tap(find.byType(FileBlockComponent));
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/core/config/kv.dart';
|
import 'package:appflowy/core/config/kv.dart';
|
||||||
import 'package:appflowy/core/config/kv_keys.dart';
|
import 'package:appflowy/core/config/kv_keys.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
@ -17,6 +14,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'
|
|||||||
hide UploadImageMenu, ResizableImage;
|
hide UploadImageMenu, ResizableImage;
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
@ -43,7 +42,9 @@ void main() {
|
|||||||
// tap the first line of the document
|
// tap the first line of the document
|
||||||
await tester.editor.tapLineOfEditorAt(0);
|
await tester.editor.tapLineOfEditorAt(0);
|
||||||
await tester.editor.showSlashMenu();
|
await tester.editor.showSlashMenu();
|
||||||
await tester.editor.tapSlashMenuItemWithName('Image');
|
await tester.editor.tapSlashMenuItemWithName(
|
||||||
|
LocaleKeys.document_slashMenu_name_image.tr(),
|
||||||
|
);
|
||||||
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
||||||
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
@ -91,7 +92,9 @@ void main() {
|
|||||||
// tap the first line of the document
|
// tap the first line of the document
|
||||||
await tester.editor.tapLineOfEditorAt(0);
|
await tester.editor.tapLineOfEditorAt(0);
|
||||||
await tester.editor.showSlashMenu();
|
await tester.editor.showSlashMenu();
|
||||||
await tester.editor.tapSlashMenuItemWithName('Image');
|
await tester.editor.tapSlashMenuItemWithName(
|
||||||
|
LocaleKeys.document_slashMenu_name_image.tr(),
|
||||||
|
);
|
||||||
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
||||||
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
@ -144,7 +147,9 @@ void main() {
|
|||||||
// tap the first line of the document
|
// tap the first line of the document
|
||||||
await tester.editor.tapLineOfEditorAt(0);
|
await tester.editor.tapLineOfEditorAt(0);
|
||||||
await tester.editor.showSlashMenu();
|
await tester.editor.showSlashMenu();
|
||||||
await tester.editor.tapSlashMenuItemWithName('Image');
|
await tester.editor.tapSlashMenuItemWithName(
|
||||||
|
LocaleKeys.document_slashMenu_name_image.tr(),
|
||||||
|
);
|
||||||
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
||||||
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
@ -175,7 +180,9 @@ void main() {
|
|||||||
// tap the first line of the document
|
// tap the first line of the document
|
||||||
await tester.editor.tapLineOfEditorAt(0);
|
await tester.editor.tapLineOfEditorAt(0);
|
||||||
await tester.editor.showSlashMenu();
|
await tester.editor.showSlashMenu();
|
||||||
await tester.editor.tapSlashMenuItemWithName('Image');
|
await tester.editor.tapSlashMenuItemWithName(
|
||||||
|
LocaleKeys.document_slashMenu_name_image.tr(),
|
||||||
|
);
|
||||||
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
||||||
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/core/config/kv.dart';
|
import 'package:appflowy/core/config/kv.dart';
|
||||||
import 'package:appflowy/core/config/kv_keys.dart';
|
import 'package:appflowy/core/config/kv_keys.dart';
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
@ -20,6 +16,9 @@ import 'package:appflowy/startup/startup.dart';
|
|||||||
import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_toolbar.dart';
|
import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_toolbar.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';
|
import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
@ -49,7 +48,10 @@ void main() {
|
|||||||
// tap the first line of the document
|
// tap the first line of the document
|
||||||
await tester.editor.tapLineOfEditorAt(0);
|
await tester.editor.tapLineOfEditorAt(0);
|
||||||
await tester.editor.showSlashMenu();
|
await tester.editor.showSlashMenu();
|
||||||
await tester.editor.tapSlashMenuItemWithName('Photo gallery');
|
await tester.editor.tapSlashMenuItemWithName(
|
||||||
|
LocaleKeys.document_slashMenu_name_photoGallery.tr(),
|
||||||
|
offset: 100,
|
||||||
|
);
|
||||||
expect(find.byType(MultiImageBlockComponent), findsOneWidget);
|
expect(find.byType(MultiImageBlockComponent), findsOneWidget);
|
||||||
expect(find.byType(MultiImagePlaceholder), findsOneWidget);
|
expect(find.byType(MultiImagePlaceholder), findsOneWidget);
|
||||||
|
|
||||||
@ -144,7 +146,10 @@ void main() {
|
|||||||
// tap the first line of the document
|
// tap the first line of the document
|
||||||
await tester.editor.tapLineOfEditorAt(0);
|
await tester.editor.tapLineOfEditorAt(0);
|
||||||
await tester.editor.showSlashMenu();
|
await tester.editor.showSlashMenu();
|
||||||
await tester.editor.tapSlashMenuItemWithName('Photo gallery');
|
await tester.editor.tapSlashMenuItemWithName(
|
||||||
|
LocaleKeys.document_slashMenu_name_photoGallery.tr(),
|
||||||
|
offset: 100,
|
||||||
|
);
|
||||||
expect(find.byType(MultiImageBlockComponent), findsOneWidget);
|
expect(find.byType(MultiImageBlockComponent), findsOneWidget);
|
||||||
expect(find.byType(MultiImagePlaceholder), findsOneWidget);
|
expect(find.byType(MultiImagePlaceholder), findsOneWidget);
|
||||||
|
|
||||||
|
@ -171,7 +171,8 @@ Future<void> insertOutlineInDocument(WidgetTester tester) async {
|
|||||||
// open the actions menu and insert the outline block
|
// open the actions menu and insert the outline block
|
||||||
await tester.editor.showSlashMenu();
|
await tester.editor.showSlashMenu();
|
||||||
await tester.editor.tapSlashMenuItemWithName(
|
await tester.editor.tapSlashMenuItemWithName(
|
||||||
LocaleKeys.document_selectionMenu_outline.tr(),
|
LocaleKeys.document_slashMenu_name_outline.tr(),
|
||||||
|
offset: 100,
|
||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
@ -662,4 +662,34 @@ extension ViewLayoutPBTest on ViewLayoutPB {
|
|||||||
throw UnsupportedError('Unsupported layout: $this');
|
throw UnsupportedError('Unsupported layout: $this');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get slashMenuName {
|
||||||
|
switch (this) {
|
||||||
|
case ViewLayoutPB.Grid:
|
||||||
|
return LocaleKeys.document_slashMenu_name_grid.tr();
|
||||||
|
case ViewLayoutPB.Board:
|
||||||
|
return LocaleKeys.document_slashMenu_name_kanban.tr();
|
||||||
|
case ViewLayoutPB.Document:
|
||||||
|
return LocaleKeys.document_slashMenu_name_doc.tr();
|
||||||
|
case ViewLayoutPB.Calendar:
|
||||||
|
return LocaleKeys.document_slashMenu_name_calendar.tr();
|
||||||
|
default:
|
||||||
|
throw UnsupportedError('Unsupported layout: $this');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get slashMenuLinkedName {
|
||||||
|
switch (this) {
|
||||||
|
case ViewLayoutPB.Grid:
|
||||||
|
return LocaleKeys.document_slashMenu_name_linkedGrid.tr();
|
||||||
|
case ViewLayoutPB.Board:
|
||||||
|
return LocaleKeys.document_slashMenu_name_linkedKanban.tr();
|
||||||
|
case ViewLayoutPB.Document:
|
||||||
|
return LocaleKeys.document_slashMenu_name_linkedDoc.tr();
|
||||||
|
case ViewLayoutPB.Calendar:
|
||||||
|
return LocaleKeys.document_slashMenu_name_linkedCalendar.tr();
|
||||||
|
default:
|
||||||
|
throw UnsupportedError('Unsupported layout: $this');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ import 'dart:ui';
|
|||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
|
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
|
||||||
import 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.dart';
|
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
|
||||||
@ -11,6 +10,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/header/doc
|
|||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';
|
||||||
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
|
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
|
||||||
|
import 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.dart';
|
||||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@ -170,7 +170,10 @@ class EditorOperations {
|
|||||||
/// Tap the slash menu item with [name]
|
/// Tap the slash menu item with [name]
|
||||||
///
|
///
|
||||||
/// Must call [showSlashMenu] first.
|
/// Must call [showSlashMenu] first.
|
||||||
Future<void> tapSlashMenuItemWithName(String name) async {
|
Future<void> tapSlashMenuItemWithName(
|
||||||
|
String name, {
|
||||||
|
double offset = 200,
|
||||||
|
}) async {
|
||||||
final slashMenu = find
|
final slashMenu = find
|
||||||
.ancestor(
|
.ancestor(
|
||||||
of: find.byType(SelectionMenuItemWidget),
|
of: find.byType(SelectionMenuItemWidget),
|
||||||
@ -180,8 +183,13 @@ class EditorOperations {
|
|||||||
)
|
)
|
||||||
.first;
|
.first;
|
||||||
final slashMenuItem = find.text(name, findRichText: true);
|
final slashMenuItem = find.text(name, findRichText: true);
|
||||||
await tester.scrollUntilVisible(slashMenuItem, 200, scrollable: slashMenu);
|
await tester.scrollUntilVisible(
|
||||||
// await tester.ensureVisible(slashMenuItem);
|
slashMenuItem,
|
||||||
|
offset,
|
||||||
|
scrollable: slashMenu,
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
);
|
||||||
|
assert(slashMenuItem.hasFound);
|
||||||
await tester.tapButton(slashMenuItem);
|
await tester.tapButton(slashMenuItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,6 +102,7 @@ class _TypeOptionMenuItem<T> extends StatelessWidget {
|
|||||||
value.text,
|
value.text,
|
||||||
fontSize: 14.0,
|
fontSize: 14.0,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
|
lineHeight: 1.0,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
@ -133,7 +133,7 @@ enum DatabaseViewSettings {
|
|||||||
filter => FlowySvgs.filter_s,
|
filter => FlowySvgs.filter_s,
|
||||||
sort => FlowySvgs.sort_ascending_s,
|
sort => FlowySvgs.sort_ascending_s,
|
||||||
board => FlowySvgs.board_s,
|
board => FlowySvgs.board_s,
|
||||||
calendar => FlowySvgs.date_s,
|
calendar => FlowySvgs.calendar_s,
|
||||||
duplicate => FlowySvgs.copy_s,
|
duplicate => FlowySvgs.copy_s,
|
||||||
delete => FlowySvgs.delete_s,
|
delete => FlowySvgs.delete_s,
|
||||||
};
|
};
|
||||||
@ -176,6 +176,7 @@ class DatabaseViewSettingTile extends StatelessWidget {
|
|||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
FlowyText(
|
FlowyText(
|
||||||
|
lineHeight: 1.0,
|
||||||
databaseLayoutFromViewLayout(view.layout).layoutName,
|
databaseLayoutFromViewLayout(view.layout).layoutName,
|
||||||
color: Theme.of(context).hintColor,
|
color: Theme.of(context).hintColor,
|
||||||
),
|
),
|
||||||
|
@ -4,6 +4,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';
|
import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';
|
||||||
import 'package:appflowy/mobile/presentation/widgets/navigation_bar_button.dart';
|
import 'package:appflowy/mobile/presentation/widgets/navigation_bar_button.dart';
|
||||||
|
import 'package:appflowy/shared/red_dot.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||||
import 'package:appflowy/util/theme_extension.dart';
|
import 'package:appflowy/util/theme_extension.dart';
|
||||||
@ -162,7 +163,7 @@ class _NotificationNavigationBarItemIcon extends StatelessWidget {
|
|||||||
const Positioned(
|
const Positioned(
|
||||||
top: 2,
|
top: 2,
|
||||||
right: 4,
|
right: 4,
|
||||||
child: _RedDot(),
|
child: NotificationRedDot(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -172,25 +173,6 @@ class _NotificationNavigationBarItemIcon extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RedDot extends StatelessWidget {
|
|
||||||
const _RedDot();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
width: 6,
|
|
||||||
height: 6,
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
decoration: ShapeDecoration(
|
|
||||||
color: const Color(0xFFFF2214),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _HomePageNavigationBar extends StatelessWidget {
|
class _HomePageNavigationBar extends StatelessWidget {
|
||||||
const _HomePageNavigationBar({
|
const _HomePageNavigationBar({
|
||||||
required this.navigationShell,
|
required this.navigationShell,
|
||||||
@ -230,11 +212,13 @@ class _HomePageNavigationBar extends StatelessWidget {
|
|||||||
/// Navigate to the current location of the branch at the provided index when
|
/// Navigate to the current location of the branch at the provided index when
|
||||||
/// tapping an item in the BottomNavigationBar.
|
/// tapping an item in the BottomNavigationBar.
|
||||||
void _onTap(BuildContext context, int bottomBarIndex) {
|
void _onTap(BuildContext context, int bottomBarIndex) {
|
||||||
if (_items[bottomBarIndex].label == _addLabel) {
|
final label = _items[bottomBarIndex].label;
|
||||||
|
if (label == _addLabel) {
|
||||||
// show an add dialog
|
// show an add dialog
|
||||||
mobileCreateNewPageNotifier.value = ViewLayoutPB.Document;
|
mobileCreateNewPageNotifier.value = ViewLayoutPB.Document;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
} else if (label == _notificationLabel) {
|
||||||
|
getIt<ReminderBloc>().add(const ReminderEvent.refresh());
|
||||||
}
|
}
|
||||||
// When navigating to a new branch, it's recommended to use the goBranch
|
// When navigating to a new branch, it's recommended to use the goBranch
|
||||||
// method, as doing so makes sure the last navigation state of the
|
// method, as doing so makes sure the last navigation state of the
|
||||||
|
@ -29,6 +29,7 @@ class FontSetting extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
FlowyText(
|
FlowyText(
|
||||||
|
lineHeight: 1.0,
|
||||||
name,
|
name,
|
||||||
color: theme.colorScheme.onSurface,
|
color: theme.colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
|
@ -38,6 +38,7 @@ class _LanguageSettingGroupState extends State<LanguageSettingGroup> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
FlowyText(
|
FlowyText(
|
||||||
|
lineHeight: 1.0,
|
||||||
languageFromLocale(locale),
|
languageFromLocale(locale),
|
||||||
color: theme.colorScheme.onSurface,
|
color: theme.colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart';
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
|
||||||
@ -8,18 +9,22 @@ import 'package:fixnum/fixnum.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
import 'chat_message_service.dart';
|
||||||
|
|
||||||
part 'chat_ai_message_bloc.freezed.dart';
|
part 'chat_ai_message_bloc.freezed.dart';
|
||||||
|
|
||||||
class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||||
ChatAIMessageBloc({
|
ChatAIMessageBloc({
|
||||||
dynamic message,
|
dynamic message,
|
||||||
String? metadata,
|
String? refSourceJsonString,
|
||||||
required this.chatId,
|
required this.chatId,
|
||||||
required this.questionId,
|
required this.questionId,
|
||||||
}) : super(ChatAIMessageState.initial(
|
}) : super(
|
||||||
message,
|
ChatAIMessageState.initial(
|
||||||
chatMessageMetadataFromString(metadata),
|
message,
|
||||||
),) {
|
messageReferenceSource(refSourceJsonString),
|
||||||
|
),
|
||||||
|
) {
|
||||||
if (state.stream != null) {
|
if (state.stream != null) {
|
||||||
state.stream!.listen(
|
state.stream!.listen(
|
||||||
onData: (text) {
|
onData: (text) {
|
||||||
@ -37,9 +42,9 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
|||||||
add(const ChatAIMessageEvent.onAIResponseLimit());
|
add(const ChatAIMessageEvent.onAIResponseLimit());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onMetadata: (metadata) {
|
onMetadata: (sources) {
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
add(ChatAIMessageEvent.receiveMetadata(metadata));
|
add(ChatAIMessageEvent.receiveSources(sources));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -112,10 +117,10 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
receiveMetadata: (List<ChatMessageMetadata> metadata) {
|
receiveSources: (List<ChatMessageRefSource> sources) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
metadata: metadata,
|
sources: sources,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -136,8 +141,8 @@ class ChatAIMessageEvent with _$ChatAIMessageEvent {
|
|||||||
const factory ChatAIMessageEvent.retry() = _Retry;
|
const factory ChatAIMessageEvent.retry() = _Retry;
|
||||||
const factory ChatAIMessageEvent.retryResult(String text) = _RetryResult;
|
const factory ChatAIMessageEvent.retryResult(String text) = _RetryResult;
|
||||||
const factory ChatAIMessageEvent.onAIResponseLimit() = _OnAIResponseLimit;
|
const factory ChatAIMessageEvent.onAIResponseLimit() = _OnAIResponseLimit;
|
||||||
const factory ChatAIMessageEvent.receiveMetadata(
|
const factory ChatAIMessageEvent.receiveSources(
|
||||||
List<ChatMessageMetadata> data,
|
List<ChatMessageRefSource> sources,
|
||||||
) = _ReceiveMetadata;
|
) = _ReceiveMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,16 +152,18 @@ class ChatAIMessageState with _$ChatAIMessageState {
|
|||||||
AnswerStream? stream,
|
AnswerStream? stream,
|
||||||
required String text,
|
required String text,
|
||||||
required MessageState messageState,
|
required MessageState messageState,
|
||||||
required List<ChatMessageMetadata> metadata,
|
required List<ChatMessageRefSource> sources,
|
||||||
}) = _ChatAIMessageState;
|
}) = _ChatAIMessageState;
|
||||||
|
|
||||||
factory ChatAIMessageState.initial(
|
factory ChatAIMessageState.initial(
|
||||||
dynamic text, List<ChatMessageMetadata> metadata,) {
|
dynamic text,
|
||||||
|
List<ChatMessageRefSource> sources,
|
||||||
|
) {
|
||||||
return ChatAIMessageState(
|
return ChatAIMessageState(
|
||||||
text: text is String ? text : "",
|
text: text is String ? text : "",
|
||||||
stream: text is AnswerStream ? text : null,
|
stream: text is AnswerStream ? text : null,
|
||||||
messageState: const MessageState.ready(),
|
messageState: const MessageState.ready(),
|
||||||
metadata: metadata,
|
sources: sources,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:ffi';
|
|
||||||
import 'dart:isolate';
|
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart';
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||||
@ -19,16 +16,12 @@ import 'package:flutter_chat_types/flutter_chat_types.dart';
|
|||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:nanoid/nanoid.dart';
|
import 'package:nanoid/nanoid.dart';
|
||||||
|
|
||||||
|
import 'chat_entity.dart';
|
||||||
import 'chat_message_listener.dart';
|
import 'chat_message_listener.dart';
|
||||||
import 'chat_message_service.dart';
|
import 'chat_message_service.dart';
|
||||||
|
|
||||||
part 'chat_bloc.g.dart';
|
|
||||||
part 'chat_bloc.freezed.dart';
|
part 'chat_bloc.freezed.dart';
|
||||||
|
|
||||||
const sendMessageErrorKey = "sendMessageError";
|
|
||||||
const systemUserId = "system";
|
|
||||||
const aiResponseUserId = "0";
|
|
||||||
|
|
||||||
class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||||
ChatBloc({
|
ChatBloc({
|
||||||
required ViewPB view,
|
required ViewPB view,
|
||||||
@ -46,7 +39,8 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
final String chatId;
|
final String chatId;
|
||||||
|
|
||||||
/// The last streaming message id
|
/// The last streaming message id
|
||||||
String lastStreamMessageId = '';
|
String answerStreamMessageId = '';
|
||||||
|
String questionStreamMessageId = '';
|
||||||
|
|
||||||
/// Using a temporary map to associate the real message ID with the last streaming message ID.
|
/// Using a temporary map to associate the real message ID with the last streaming message ID.
|
||||||
///
|
///
|
||||||
@ -94,12 +88,18 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
Int64? beforeMessageId;
|
Int64? beforeMessageId;
|
||||||
final oldestMessage = _getOlderstMessage();
|
final oldestMessage = _getOlderstMessage();
|
||||||
if (oldestMessage != null) {
|
if (oldestMessage != null) {
|
||||||
beforeMessageId = Int64.parseInt(oldestMessage.id);
|
try {
|
||||||
|
beforeMessageId = Int64.parseInt(oldestMessage.id);
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(
|
||||||
|
"Failed to parse message id: $e, messaeg_id: ${oldestMessage.id}",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_loadPrevMessage(beforeMessageId);
|
_loadPrevMessage(beforeMessageId);
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
loadingPreviousStatus: const LoadingState.loading(),
|
loadingPreviousStatus: const ChatLoadingState.loading(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -115,7 +115,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
messages: uniqueMessages,
|
messages: uniqueMessages,
|
||||||
loadingPreviousStatus: const LoadingState.finish(),
|
loadingPreviousStatus: const ChatLoadingState.finish(),
|
||||||
hasMorePrevMessage: hasMore,
|
hasMorePrevMessage: hasMore,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -129,26 +129,16 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
messages: uniqueMessages,
|
messages: uniqueMessages,
|
||||||
initialLoadingStatus: const LoadingState.finish(),
|
initialLoadingStatus: const ChatLoadingState.finish(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
// streaming message
|
// streaming message
|
||||||
streaming: (Message message) {
|
finishAnswerStreaming: () {
|
||||||
final allMessages = _perminentMessages();
|
|
||||||
allMessages.insert(0, message);
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
messages: allMessages,
|
|
||||||
streamingState: const StreamingState.streaming(),
|
|
||||||
canSendMessage: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
finishStreaming: () {
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
streamingState: const StreamingState.done(),
|
streamingState: const StreamingState.done(),
|
||||||
|
acceptRelatedQuestion: true,
|
||||||
canSendMessage:
|
canSendMessage:
|
||||||
state.sendingState == const SendMessageState.done(),
|
state.sendingState == const SendMessageState.done(),
|
||||||
),
|
),
|
||||||
@ -169,9 +159,9 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
// If the streaming is not started, remove the message from the list
|
// If the streaming is not started, remove the message from the list
|
||||||
if (!state.answerStream!.hasStarted) {
|
if (!state.answerStream!.hasStarted) {
|
||||||
allMessages.removeWhere(
|
allMessages.removeWhere(
|
||||||
(element) => element.id == lastStreamMessageId,
|
(element) => element.id == answerStreamMessageId,
|
||||||
);
|
);
|
||||||
lastStreamMessageId = "";
|
answerStreamMessageId = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// when stop stream, we will set the answer stream to null. Which means the streaming
|
// when stop stream, we will set the answer stream to null. Which means the streaming
|
||||||
@ -196,6 +186,17 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
startAnswerStreaming: (Message message) {
|
||||||
|
final allMessages = _perminentMessages();
|
||||||
|
allMessages.insert(0, message);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
messages: allMessages,
|
||||||
|
streamingState: const StreamingState.streaming(),
|
||||||
|
canSendMessage: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
sendMessage: (String message, Map<String, dynamic>? metadata) async {
|
sendMessage: (String message, Map<String, dynamic>? metadata) async {
|
||||||
unawaited(_startStreamingMessage(message, metadata, emit));
|
unawaited(_startStreamingMessage(message, metadata, emit));
|
||||||
final allMessages = _perminentMessages();
|
final allMessages = _perminentMessages();
|
||||||
@ -204,6 +205,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
lastSentMessage: null,
|
lastSentMessage: null,
|
||||||
messages: allMessages,
|
messages: allMessages,
|
||||||
relatedQuestions: [],
|
relatedQuestions: [],
|
||||||
|
acceptRelatedQuestion: false,
|
||||||
sendingState: const SendMessageState.sending(),
|
sendingState: const SendMessageState.sending(),
|
||||||
canSendMessage: false,
|
canSendMessage: false,
|
||||||
),
|
),
|
||||||
@ -256,10 +258,17 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
chatMessageCallback: (pb) {
|
chatMessageCallback: (pb) {
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
// 3 mean message response from AI
|
// 3 mean message response from AI
|
||||||
if (pb.authorType == 3 && lastStreamMessageId.isNotEmpty) {
|
if (pb.authorType == 3 && answerStreamMessageId.isNotEmpty) {
|
||||||
temporaryMessageIDMap[pb.messageId.toString()] =
|
temporaryMessageIDMap[pb.messageId.toString()] =
|
||||||
lastStreamMessageId;
|
answerStreamMessageId;
|
||||||
lastStreamMessageId = "";
|
answerStreamMessageId = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1 mean message response from User
|
||||||
|
if (pb.authorType == 1 && questionStreamMessageId.isNotEmpty) {
|
||||||
|
temporaryMessageIDMap[pb.messageId.toString()] =
|
||||||
|
questionStreamMessageId;
|
||||||
|
questionStreamMessageId = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
final message = _createTextMessage(pb);
|
final message = _createTextMessage(pb);
|
||||||
@ -269,7 +278,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
chatErrorMessageCallback: (err) {
|
chatErrorMessageCallback: (err) {
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
Log.error("chat error: ${err.errorMessage}");
|
Log.error("chat error: ${err.errorMessage}");
|
||||||
add(const ChatEvent.finishStreaming());
|
add(const ChatEvent.finishAnswerStreaming());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
latestMessageCallback: (list) {
|
latestMessageCallback: (list) {
|
||||||
@ -286,7 +295,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
},
|
},
|
||||||
finishStreamingCallback: () {
|
finishStreamingCallback: () {
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
add(const ChatEvent.finishStreaming());
|
add(const ChatEvent.finishAnswerStreaming());
|
||||||
// The answer strema will bet set to null after the streaming is finished or canceled.
|
// The answer strema will bet set to null after the streaming is finished or canceled.
|
||||||
// so if the answer stream is null, we will not get related question.
|
// so if the answer stream is null, we will not get related question.
|
||||||
if (state.lastSentMessage != null && state.answerStream != null) {
|
if (state.lastSentMessage != null && state.answerStream != null) {
|
||||||
@ -299,7 +308,9 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
result.fold(
|
result.fold(
|
||||||
(list) {
|
(list) {
|
||||||
add(ChatEvent.didReceiveRelatedQuestion(list.items));
|
if (state.acceptRelatedQuestion) {
|
||||||
|
add(ChatEvent.didReceiveRelatedQuestion(list.items));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
(err) {
|
(err) {
|
||||||
Log.error("Failed to get related question: $err");
|
Log.error("Failed to get related question: $err");
|
||||||
@ -357,16 +368,24 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final answerStream = AnswerStream();
|
final answerStream = AnswerStream();
|
||||||
|
final questionStream = QuestionStream();
|
||||||
add(ChatEvent.didUpdateAnswerStream(answerStream));
|
add(ChatEvent.didUpdateAnswerStream(answerStream));
|
||||||
|
|
||||||
final payload = StreamChatPayloadPB(
|
final payload = StreamChatPayloadPB(
|
||||||
chatId: state.view.id,
|
chatId: state.view.id,
|
||||||
message: message,
|
message: message,
|
||||||
messageType: ChatMessageTypePB.User,
|
messageType: ChatMessageTypePB.User,
|
||||||
textStreamPort: Int64(answerStream.nativePort),
|
questionStreamPort: Int64(questionStream.nativePort),
|
||||||
|
answerStreamPort: Int64(answerStream.nativePort),
|
||||||
metadata: await metadataPBFromMetadata(metadata),
|
metadata: await metadataPBFromMetadata(metadata),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final questionStreamMessage = _createQuestionStreamMessage(
|
||||||
|
questionStream,
|
||||||
|
metadata,
|
||||||
|
);
|
||||||
|
add(ChatEvent.receveMessage(questionStreamMessage));
|
||||||
|
|
||||||
// Stream message to the server
|
// Stream message to the server
|
||||||
final result = await AIEventStreamMessage(payload).send();
|
final result = await AIEventStreamMessage(payload).send();
|
||||||
result.fold(
|
result.fold(
|
||||||
@ -374,13 +393,12 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
add(ChatEvent.finishSending(question));
|
add(ChatEvent.finishSending(question));
|
||||||
|
|
||||||
final questionMessageId = question.messageId;
|
// final message = _createTextMessage(question);
|
||||||
final message = _createTextMessage(question);
|
// add(ChatEvent.receveMessage(message));
|
||||||
add(ChatEvent.receveMessage(message));
|
|
||||||
|
|
||||||
final streamAnswer =
|
final streamAnswer =
|
||||||
_createStreamMessage(answerStream, questionMessageId);
|
_createAnswerStreamMessage(answerStream, question.messageId);
|
||||||
add(ChatEvent.streaming(streamAnswer));
|
add(ChatEvent.startAnswerStreaming(streamAnswer));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(err) {
|
(err) {
|
||||||
@ -403,16 +421,18 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Message _createStreamMessage(AnswerStream stream, Int64 questionMessageId) {
|
Message _createAnswerStreamMessage(
|
||||||
|
AnswerStream stream,
|
||||||
|
Int64 questionMessageId,
|
||||||
|
) {
|
||||||
final streamMessageId = (questionMessageId + 1).toString();
|
final streamMessageId = (questionMessageId + 1).toString();
|
||||||
|
answerStreamMessageId = streamMessageId;
|
||||||
lastStreamMessageId = streamMessageId;
|
|
||||||
|
|
||||||
return TextMessage(
|
return TextMessage(
|
||||||
author: User(id: "streamId:${nanoid()}"),
|
author: User(id: "streamId:${nanoid()}"),
|
||||||
metadata: {
|
metadata: {
|
||||||
"$AnswerStream": stream,
|
"$AnswerStream": stream,
|
||||||
"question": questionMessageId,
|
messageQuestionIdKey: questionMessageId,
|
||||||
"chatId": chatId,
|
"chatId": chatId,
|
||||||
},
|
},
|
||||||
id: streamMessageId,
|
id: streamMessageId,
|
||||||
@ -421,6 +441,32 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message _createQuestionStreamMessage(
|
||||||
|
QuestionStream stream,
|
||||||
|
Map<String, dynamic>? sentMetadata,
|
||||||
|
) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final timestamp = now.millisecondsSinceEpoch;
|
||||||
|
questionStreamMessageId = timestamp.toString();
|
||||||
|
final Map<String, dynamic> metadata = {};
|
||||||
|
|
||||||
|
// if (sentMetadata != null) {
|
||||||
|
// metadata[messageMetadataJsonStringKey] = sentMetadata;
|
||||||
|
// }
|
||||||
|
|
||||||
|
metadata["$QuestionStream"] = stream;
|
||||||
|
metadata["chatId"] = chatId;
|
||||||
|
metadata[messageChatFileListKey] =
|
||||||
|
chatFilesFromMessageMetadata(sentMetadata);
|
||||||
|
return TextMessage(
|
||||||
|
author: User(id: state.userProfile.id.toString()),
|
||||||
|
metadata: metadata,
|
||||||
|
id: questionStreamMessageId,
|
||||||
|
createdAt: DateTime.now().millisecondsSinceEpoch,
|
||||||
|
text: '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Message _createTextMessage(ChatMessagePB message) {
|
Message _createTextMessage(ChatMessagePB message) {
|
||||||
String messageId = message.messageId.toString();
|
String messageId = message.messageId.toString();
|
||||||
|
|
||||||
@ -435,7 +481,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
text: message.content,
|
text: message.content,
|
||||||
createdAt: message.createdAt.toInt() * 1000,
|
createdAt: message.createdAt.toInt() * 1000,
|
||||||
metadata: {
|
metadata: {
|
||||||
"metadata": message.metadata,
|
messageRefSourceJsonStringKey: message.metadata,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -454,9 +500,10 @@ class ChatEvent with _$ChatEvent {
|
|||||||
_FinishSendMessage;
|
_FinishSendMessage;
|
||||||
|
|
||||||
// receive message
|
// receive message
|
||||||
const factory ChatEvent.streaming(Message message) = _StreamingMessage;
|
const factory ChatEvent.startAnswerStreaming(Message message) =
|
||||||
|
_StartAnswerStreaming;
|
||||||
const factory ChatEvent.receveMessage(Message message) = _ReceiveMessage;
|
const factory ChatEvent.receveMessage(Message message) = _ReceiveMessage;
|
||||||
const factory ChatEvent.finishStreaming() = _FinishStreamingMessage;
|
const factory ChatEvent.finishAnswerStreaming() = _FinishAnswerStreaming;
|
||||||
|
|
||||||
// loading messages
|
// loading messages
|
||||||
const factory ChatEvent.startLoadingPrevMessage() = _StartLoadPrevMessage;
|
const factory ChatEvent.startLoadingPrevMessage() = _StartLoadPrevMessage;
|
||||||
@ -487,10 +534,10 @@ class ChatState with _$ChatState {
|
|||||||
required UserProfilePB userProfile,
|
required UserProfilePB userProfile,
|
||||||
// When opening the chat, the initial loading status will be set as loading.
|
// When opening the chat, the initial loading status will be set as loading.
|
||||||
//After the initial loading is done, the status will be set as finished.
|
//After the initial loading is done, the status will be set as finished.
|
||||||
required LoadingState initialLoadingStatus,
|
required ChatLoadingState initialLoadingStatus,
|
||||||
// When loading previous messages, the status will be set as loading.
|
// When loading previous messages, the status will be set as loading.
|
||||||
// After the loading is done, the status will be set as finished.
|
// After the loading is done, the status will be set as finished.
|
||||||
required LoadingState loadingPreviousStatus,
|
required ChatLoadingState loadingPreviousStatus,
|
||||||
// When sending a user message, the status will be set as loading.
|
// When sending a user message, the status will be set as loading.
|
||||||
// After the message is sent, the status will be set as finished.
|
// After the message is sent, the status will be set as finished.
|
||||||
required StreamingState streamingState,
|
required StreamingState streamingState,
|
||||||
@ -499,6 +546,7 @@ class ChatState with _$ChatState {
|
|||||||
required bool hasMorePrevMessage,
|
required bool hasMorePrevMessage,
|
||||||
// The related questions that are received after the user message is sent.
|
// The related questions that are received after the user message is sent.
|
||||||
required List<RelatedQuestionPB> relatedQuestions,
|
required List<RelatedQuestionPB> relatedQuestions,
|
||||||
|
@Default(false) bool acceptRelatedQuestion,
|
||||||
// The last user message that is sent to the server.
|
// The last user message that is sent to the server.
|
||||||
ChatMessagePB? lastSentMessage,
|
ChatMessagePB? lastSentMessage,
|
||||||
AnswerStream? answerStream,
|
AnswerStream? answerStream,
|
||||||
@ -510,8 +558,8 @@ class ChatState with _$ChatState {
|
|||||||
view: view,
|
view: view,
|
||||||
messages: [],
|
messages: [],
|
||||||
userProfile: userProfile,
|
userProfile: userProfile,
|
||||||
initialLoadingStatus: const LoadingState.finish(),
|
initialLoadingStatus: const ChatLoadingState.finish(),
|
||||||
loadingPreviousStatus: const LoadingState.finish(),
|
loadingPreviousStatus: const ChatLoadingState.finish(),
|
||||||
streamingState: const StreamingState.done(),
|
streamingState: const StreamingState.done(),
|
||||||
sendingState: const SendMessageState.done(),
|
sendingState: const SendMessageState.done(),
|
||||||
hasMorePrevMessage: true,
|
hasMorePrevMessage: true,
|
||||||
@ -524,202 +572,3 @@ bool isOtherUserMessage(Message message) {
|
|||||||
message.author.id != systemUserId &&
|
message.author.id != systemUserId &&
|
||||||
!message.author.id.startsWith("streamId:");
|
!message.author.id.startsWith("streamId:");
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
|
||||||
class LoadingState with _$LoadingState {
|
|
||||||
const factory LoadingState.loading() = _Loading;
|
|
||||||
const factory LoadingState.finish({FlowyError? error}) = _Finish;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OnetimeShotType {
|
|
||||||
unknown,
|
|
||||||
sendingMessage,
|
|
||||||
relatedQuestion,
|
|
||||||
invalidSendMesssage,
|
|
||||||
}
|
|
||||||
|
|
||||||
const onetimeShotType = "OnetimeShotType";
|
|
||||||
|
|
||||||
extension OnetimeMessageTypeExtension on OnetimeShotType {
|
|
||||||
static OnetimeShotType fromString(String value) {
|
|
||||||
switch (value) {
|
|
||||||
case 'OnetimeShotType.relatedQuestion':
|
|
||||||
return OnetimeShotType.relatedQuestion;
|
|
||||||
case 'OnetimeShotType.invalidSendMesssage':
|
|
||||||
return OnetimeShotType.invalidSendMesssage;
|
|
||||||
default:
|
|
||||||
Log.error('Unknown OnetimeShotType: $value');
|
|
||||||
return OnetimeShotType.unknown;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
onetimeShotType: toString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OnetimeShotType? onetimeMessageTypeFromMeta(Map<String, dynamic>? metadata) {
|
|
||||||
if (metadata == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final entry in metadata.entries) {
|
|
||||||
if (entry.key == onetimeShotType) {
|
|
||||||
return OnetimeMessageTypeExtension.fromString(entry.value as String);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
class AnswerStream {
|
|
||||||
AnswerStream() {
|
|
||||||
_port.handler = _controller.add;
|
|
||||||
_subscription = _controller.stream.listen(
|
|
||||||
(event) {
|
|
||||||
if (event.startsWith("data:")) {
|
|
||||||
_hasStarted = true;
|
|
||||||
final newText = event.substring(5);
|
|
||||||
_text += newText;
|
|
||||||
if (_onData != null) {
|
|
||||||
_onData!(_text);
|
|
||||||
}
|
|
||||||
} else if (event.startsWith("error:")) {
|
|
||||||
_error = event.substring(5);
|
|
||||||
if (_onError != null) {
|
|
||||||
_onError!(_error!);
|
|
||||||
}
|
|
||||||
} else if (event.startsWith("metadata:")) {
|
|
||||||
if (_onMetadata != null) {
|
|
||||||
final s = event.substring(9);
|
|
||||||
_onMetadata!(chatMessageMetadataFromString(s));
|
|
||||||
}
|
|
||||||
} else if (event == "AI_RESPONSE_LIMIT") {
|
|
||||||
if (_onAIResponseLimit != null) {
|
|
||||||
_onAIResponseLimit!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDone: () {
|
|
||||||
if (_onEnd != null) {
|
|
||||||
_onEnd!();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error) {
|
|
||||||
if (_onError != null) {
|
|
||||||
_onError!(error.toString());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final RawReceivePort _port = RawReceivePort();
|
|
||||||
final StreamController<String> _controller = StreamController.broadcast();
|
|
||||||
late StreamSubscription<String> _subscription;
|
|
||||||
bool _hasStarted = false;
|
|
||||||
String? _error;
|
|
||||||
String _text = "";
|
|
||||||
|
|
||||||
// Callbacks
|
|
||||||
void Function(String text)? _onData;
|
|
||||||
void Function()? _onStart;
|
|
||||||
void Function()? _onEnd;
|
|
||||||
void Function(String error)? _onError;
|
|
||||||
void Function()? _onAIResponseLimit;
|
|
||||||
void Function(List<ChatMessageMetadata> metadata)? _onMetadata;
|
|
||||||
|
|
||||||
int get nativePort => _port.sendPort.nativePort;
|
|
||||||
bool get hasStarted => _hasStarted;
|
|
||||||
String? get error => _error;
|
|
||||||
String get text => _text;
|
|
||||||
|
|
||||||
Future<void> dispose() async {
|
|
||||||
await _controller.close();
|
|
||||||
await _subscription.cancel();
|
|
||||||
_port.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void listen({
|
|
||||||
void Function(String text)? onData,
|
|
||||||
void Function()? onStart,
|
|
||||||
void Function()? onEnd,
|
|
||||||
void Function(String error)? onError,
|
|
||||||
void Function()? onAIResponseLimit,
|
|
||||||
void Function(List<ChatMessageMetadata> metadata)? onMetadata,
|
|
||||||
}) {
|
|
||||||
_onData = onData;
|
|
||||||
_onStart = onStart;
|
|
||||||
_onEnd = onEnd;
|
|
||||||
_onError = onError;
|
|
||||||
_onAIResponseLimit = onAIResponseLimit;
|
|
||||||
_onMetadata = onMetadata;
|
|
||||||
|
|
||||||
if (_onStart != null) {
|
|
||||||
_onStart!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<ChatMessageMetadata> chatMessageMetadataFromString(String? s) {
|
|
||||||
if (s == null || s.isEmpty || s == "null") {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<ChatMessageMetadata> metadata = [];
|
|
||||||
try {
|
|
||||||
final metadataJson = jsonDecode(s);
|
|
||||||
if (metadataJson == null) {
|
|
||||||
Log.warn("metadata is null");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadataJson is Map<String, dynamic>) {
|
|
||||||
if (metadataJson.isNotEmpty) {
|
|
||||||
metadata.add(ChatMessageMetadata.fromJson(metadataJson));
|
|
||||||
}
|
|
||||||
} else if (metadataJson is List) {
|
|
||||||
metadata.addAll(
|
|
||||||
metadataJson.map(
|
|
||||||
(e) => ChatMessageMetadata.fromJson(e as Map<String, dynamic>),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Log.error("Invalid metadata: $metadataJson");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
Log.error("Failed to parse metadata: $e");
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonSerializable()
|
|
||||||
class ChatMessageMetadata {
|
|
||||||
ChatMessageMetadata({
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
required this.source,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory ChatMessageMetadata.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$ChatMessageMetadataFromJson(json);
|
|
||||||
|
|
||||||
final String id;
|
|
||||||
final String name;
|
|
||||||
final String source;
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$ChatMessageMetadataToJson(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class StreamingState with _$StreamingState {
|
|
||||||
const factory StreamingState.streaming() = _Streaming;
|
|
||||||
const factory StreamingState.done({FlowyError? error}) = _StreamDone;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class SendMessageState with _$SendMessageState {
|
|
||||||
const factory SendMessageState.sending() = _Sending;
|
|
||||||
const factory SendMessageState.done({FlowyError? error}) = _SendDone;
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,182 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbenum.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
part 'chat_entity.g.dart';
|
||||||
|
part 'chat_entity.freezed.dart';
|
||||||
|
|
||||||
|
const sendMessageErrorKey = "sendMessageError";
|
||||||
|
const systemUserId = "system";
|
||||||
|
const aiResponseUserId = "0";
|
||||||
|
|
||||||
|
/// `messageRefSourceJsonStringKey` is the key used for metadata that contains the reference source of a message.
|
||||||
|
/// Each message may include this information.
|
||||||
|
/// - When used in a sent message, it indicates that the message includes an attachment.
|
||||||
|
/// - When used in a received message, it indicates the AI reference sources used to answer a question.
|
||||||
|
const messageRefSourceJsonStringKey = "ref_source_json_string";
|
||||||
|
const messageChatFileListKey = "chat_files";
|
||||||
|
const messageQuestionIdKey = "question_id";
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class ChatMessageRefSource {
|
||||||
|
ChatMessageRefSource({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.source,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ChatMessageRefSource.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ChatMessageRefSourceFromJson(json);
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String source;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$ChatMessageRefSourceToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class StreamingState with _$StreamingState {
|
||||||
|
const factory StreamingState.streaming() = _Streaming;
|
||||||
|
const factory StreamingState.done({FlowyError? error}) = _StreamDone;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SendMessageState with _$SendMessageState {
|
||||||
|
const factory SendMessageState.sending() = _Sending;
|
||||||
|
const factory SendMessageState.done({FlowyError? error}) = _SendDone;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatFile extends Equatable {
|
||||||
|
const ChatFile({
|
||||||
|
required this.filePath,
|
||||||
|
required this.fileName,
|
||||||
|
required this.fileType,
|
||||||
|
});
|
||||||
|
|
||||||
|
static ChatFile? fromFilePath(String filePath) {
|
||||||
|
final file = File(filePath);
|
||||||
|
if (!file.existsSync()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final fileName = path.basename(filePath);
|
||||||
|
final extension = path.extension(filePath).toLowerCase();
|
||||||
|
|
||||||
|
ChatMessageMetaTypePB fileType;
|
||||||
|
switch (extension) {
|
||||||
|
case '.pdf':
|
||||||
|
fileType = ChatMessageMetaTypePB.PDF;
|
||||||
|
break;
|
||||||
|
case '.txt':
|
||||||
|
fileType = ChatMessageMetaTypePB.Txt;
|
||||||
|
break;
|
||||||
|
case '.md':
|
||||||
|
fileType = ChatMessageMetaTypePB.Markdown;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fileType = ChatMessageMetaTypePB.UnknownMetaType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ChatFile(
|
||||||
|
filePath: filePath,
|
||||||
|
fileName: fileName,
|
||||||
|
fileType: fileType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String filePath;
|
||||||
|
final String fileName;
|
||||||
|
final ChatMessageMetaTypePB fileType;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [filePath];
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ChatFileTypeExtension on ChatMessageMetaTypePB {
|
||||||
|
Widget get icon {
|
||||||
|
switch (this) {
|
||||||
|
case ChatMessageMetaTypePB.PDF:
|
||||||
|
return const FlowySvg(
|
||||||
|
FlowySvgs.file_pdf_s,
|
||||||
|
color: Color(0xff00BCF0),
|
||||||
|
);
|
||||||
|
case ChatMessageMetaTypePB.Txt:
|
||||||
|
return const FlowySvg(
|
||||||
|
FlowySvgs.file_txt_s,
|
||||||
|
color: Color(0xff00BCF0),
|
||||||
|
);
|
||||||
|
case ChatMessageMetaTypePB.Markdown:
|
||||||
|
return const FlowySvg(
|
||||||
|
FlowySvgs.file_md_s,
|
||||||
|
color: Color(0xff00BCF0),
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return const FlowySvg(FlowySvgs.file_unknown_s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef ChatInputFileMetadata = Map<String, ChatFile>;
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ChatLoadingState with _$ChatLoadingState {
|
||||||
|
const factory ChatLoadingState.loading() = _Loading;
|
||||||
|
const factory ChatLoadingState.finish({FlowyError? error}) = _Finish;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ChatLoadingStateExtension on ChatLoadingState {
|
||||||
|
bool get isLoading => this is _Loading;
|
||||||
|
bool get isFinish => this is _Finish;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OnetimeShotType {
|
||||||
|
unknown,
|
||||||
|
sendingMessage,
|
||||||
|
relatedQuestion,
|
||||||
|
invalidSendMesssage,
|
||||||
|
}
|
||||||
|
|
||||||
|
const onetimeShotType = "OnetimeShotType";
|
||||||
|
|
||||||
|
extension OnetimeMessageTypeExtension on OnetimeShotType {
|
||||||
|
static OnetimeShotType fromString(String value) {
|
||||||
|
switch (value) {
|
||||||
|
case 'OnetimeShotType.sendingMessage':
|
||||||
|
return OnetimeShotType.sendingMessage;
|
||||||
|
case 'OnetimeShotType.relatedQuestion':
|
||||||
|
return OnetimeShotType.relatedQuestion;
|
||||||
|
case 'OnetimeShotType.invalidSendMesssage':
|
||||||
|
return OnetimeShotType.invalidSendMesssage;
|
||||||
|
default:
|
||||||
|
Log.error('Unknown OnetimeShotType: $value');
|
||||||
|
return OnetimeShotType.unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
onetimeShotType: toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OnetimeShotType? onetimeMessageTypeFromMeta(Map<String, dynamic>? metadata) {
|
||||||
|
if (metadata == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final entry in metadata.entries) {
|
||||||
|
if (entry.key == onetimeShotType) {
|
||||||
|
return OnetimeMessageTypeExtension.fromString(entry.value as String);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
|
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
@ -12,9 +13,8 @@ import 'chat_input_bloc.dart';
|
|||||||
part 'chat_file_bloc.freezed.dart';
|
part 'chat_file_bloc.freezed.dart';
|
||||||
|
|
||||||
class ChatFileBloc extends Bloc<ChatFileEvent, ChatFileState> {
|
class ChatFileBloc extends Bloc<ChatFileEvent, ChatFileState> {
|
||||||
ChatFileBloc({
|
ChatFileBloc()
|
||||||
required String chatId,
|
: listener = LocalLLMListener(),
|
||||||
}) : listener = LocalLLMListener(),
|
|
||||||
super(const ChatFileState()) {
|
super(const ChatFileState()) {
|
||||||
listener.start(
|
listener.start(
|
||||||
stateCallback: (pluginState) {
|
stateCallback: (pluginState) {
|
||||||
@ -49,38 +49,15 @@ class ChatFileBloc extends Bloc<ChatFileEvent, ChatFileState> {
|
|||||||
},
|
},
|
||||||
newFile: (String filePath, String fileName) async {
|
newFile: (String filePath, String fileName) async {
|
||||||
final files = List<ChatFile>.from(state.uploadFiles);
|
final files = List<ChatFile>.from(state.uploadFiles);
|
||||||
files.add(ChatFile(filePath: filePath, fileName: fileName));
|
final newFile = ChatFile.fromFilePath(filePath);
|
||||||
emit(
|
if (newFile != null) {
|
||||||
state.copyWith(
|
files.add(newFile);
|
||||||
uploadFiles: files,
|
emit(
|
||||||
),
|
state.copyWith(
|
||||||
);
|
uploadFiles: files,
|
||||||
|
),
|
||||||
emit(
|
);
|
||||||
state.copyWith(
|
}
|
||||||
uploadFileIndicator: UploadFileIndicator.uploading(fileName),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final payload = ChatFilePB(filePath: filePath, chatId: chatId);
|
|
||||||
unawaited(
|
|
||||||
AIEventChatWithFile(payload).send().then((result) {
|
|
||||||
if (!isClosed) {
|
|
||||||
result.fold((_) {
|
|
||||||
add(
|
|
||||||
ChatFileEvent.updateUploadState(
|
|
||||||
UploadFileIndicator.finish(fileName),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}, (err) {
|
|
||||||
add(
|
|
||||||
ChatFileEvent.updateUploadState(
|
|
||||||
UploadFileIndicator.error(err.msg),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
updateChatState: (LocalAIChatPB chatState) {
|
updateChatState: (LocalAIChatPB chatState) {
|
||||||
// Only user enable chat with file and the plugin is already running
|
// Only user enable chat with file and the plugin is already running
|
||||||
@ -109,6 +86,15 @@ class ChatFileBloc extends Bloc<ChatFileEvent, ChatFileState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
deleteFile: (file) {
|
||||||
|
final files = List<ChatFile>.from(state.uploadFiles);
|
||||||
|
files.remove(file);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
uploadFiles: files,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
clear: () {
|
clear: () {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@ -116,14 +102,24 @@ class ChatFileBloc extends Bloc<ChatFileEvent, ChatFileState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
updateUploadState: (UploadFileIndicator indicator) {
|
|
||||||
emit(state.copyWith(uploadFileIndicator: indicator));
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ChatInputFileMetadata consumeMetaData() {
|
||||||
|
final metadata = state.uploadFiles.fold(
|
||||||
|
<String, ChatFile>{},
|
||||||
|
(map, file) => map..putIfAbsent(file.filePath, () => file),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (metadata.isNotEmpty) {
|
||||||
|
add(const ChatFileEvent.clear());
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
final LocalLLMListener listener;
|
final LocalLLMListener listener;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -138,9 +134,8 @@ class ChatFileEvent with _$ChatFileEvent {
|
|||||||
const factory ChatFileEvent.initial() = Initial;
|
const factory ChatFileEvent.initial() = Initial;
|
||||||
const factory ChatFileEvent.newFile(String filePath, String fileName) =
|
const factory ChatFileEvent.newFile(String filePath, String fileName) =
|
||||||
_NewFile;
|
_NewFile;
|
||||||
|
const factory ChatFileEvent.deleteFile(ChatFile file) = _DeleteFile;
|
||||||
const factory ChatFileEvent.clear() = _ClearFile;
|
const factory ChatFileEvent.clear() = _ClearFile;
|
||||||
const factory ChatFileEvent.updateUploadState(UploadFileIndicator indicator) =
|
|
||||||
_UpdateUploadState;
|
|
||||||
const factory ChatFileEvent.updateChatState(LocalAIChatPB chatState) =
|
const factory ChatFileEvent.updateChatState(LocalAIChatPB chatState) =
|
||||||
_UpdateChatState;
|
_UpdateChatState;
|
||||||
const factory ChatFileEvent.updatePluginState(
|
const factory ChatFileEvent.updatePluginState(
|
||||||
@ -152,26 +147,8 @@ class ChatFileEvent with _$ChatFileEvent {
|
|||||||
class ChatFileState with _$ChatFileState {
|
class ChatFileState with _$ChatFileState {
|
||||||
const factory ChatFileState({
|
const factory ChatFileState({
|
||||||
@Default(false) bool supportChatWithFile,
|
@Default(false) bool supportChatWithFile,
|
||||||
UploadFileIndicator? uploadFileIndicator,
|
|
||||||
LocalAIChatPB? chatState,
|
LocalAIChatPB? chatState,
|
||||||
@Default([]) List<ChatFile> uploadFiles,
|
@Default([]) List<ChatFile> uploadFiles,
|
||||||
@Default(AIType.appflowyAI()) AIType aiType,
|
@Default(AIType.appflowyAI()) AIType aiType,
|
||||||
}) = _ChatFileState;
|
}) = _ChatFileState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
|
||||||
class UploadFileIndicator with _$UploadFileIndicator {
|
|
||||||
const factory UploadFileIndicator.finish(String fileName) = _Finish;
|
|
||||||
const factory UploadFileIndicator.uploading(String fileName) = _Uploading;
|
|
||||||
const factory UploadFileIndicator.error(String error) = _Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChatFile {
|
|
||||||
ChatFile({
|
|
||||||
required this.filePath,
|
|
||||||
required this.fileName,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String filePath;
|
|
||||||
final String fileName;
|
|
||||||
}
|
|
||||||
|
@ -81,7 +81,7 @@ class ChatInputActionBloc
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
addPage: (ChatInputActionPage page) {
|
addPage: (ChatInputMention page) {
|
||||||
if (!state.selectedPages.any((p) => p.pageId == page.pageId)) {
|
if (!state.selectedPages.any((p) => p.pageId == page.pageId)) {
|
||||||
final List<ViewActionPage> pages = _filterPages(
|
final List<ViewActionPage> pages = _filterPages(
|
||||||
state.views,
|
state.views,
|
||||||
@ -97,7 +97,7 @@ class ChatInputActionBloc
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
removePage: (String text) {
|
removePage: (String text) {
|
||||||
final List<ChatInputActionPage> selectedPages =
|
final List<ChatInputMention> selectedPages =
|
||||||
List.from(state.selectedPages);
|
List.from(state.selectedPages);
|
||||||
selectedPages.retainWhere((t) => !text.contains(t.title));
|
selectedPages.retainWhere((t) => !text.contains(t.title));
|
||||||
|
|
||||||
@ -128,7 +128,7 @@ class ChatInputActionBloc
|
|||||||
|
|
||||||
List<ViewActionPage> _filterPages(
|
List<ViewActionPage> _filterPages(
|
||||||
List<ViewPB> views,
|
List<ViewPB> views,
|
||||||
List<ChatInputActionPage> selectedPages,
|
List<ChatInputMention> selectedPages,
|
||||||
String filter,
|
String filter,
|
||||||
) {
|
) {
|
||||||
final pages = views
|
final pages = views
|
||||||
@ -152,7 +152,7 @@ List<ViewActionPage> _filterPages(
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ViewActionPage extends ChatInputActionPage {
|
class ViewActionPage extends ChatInputMention {
|
||||||
ViewActionPage({required this.view});
|
ViewActionPage({required this.view});
|
||||||
|
|
||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
@ -182,8 +182,7 @@ class ChatInputActionEvent with _$ChatInputActionEvent {
|
|||||||
const factory ChatInputActionEvent.handleKeyEvent(
|
const factory ChatInputActionEvent.handleKeyEvent(
|
||||||
PhysicalKeyboardKey keyboardKey,
|
PhysicalKeyboardKey keyboardKey,
|
||||||
) = _HandleKeyEvent;
|
) = _HandleKeyEvent;
|
||||||
const factory ChatInputActionEvent.addPage(ChatInputActionPage page) =
|
const factory ChatInputActionEvent.addPage(ChatInputMention page) = _AddPage;
|
||||||
_AddPage;
|
|
||||||
const factory ChatInputActionEvent.removePage(String text) = _RemovePage;
|
const factory ChatInputActionEvent.removePage(String text) = _RemovePage;
|
||||||
const factory ChatInputActionEvent.clear() = _Clear;
|
const factory ChatInputActionEvent.clear() = _Clear;
|
||||||
}
|
}
|
||||||
@ -192,8 +191,8 @@ class ChatInputActionEvent with _$ChatInputActionEvent {
|
|||||||
class ChatInputActionState with _$ChatInputActionState {
|
class ChatInputActionState with _$ChatInputActionState {
|
||||||
const factory ChatInputActionState({
|
const factory ChatInputActionState({
|
||||||
@Default([]) List<ViewPB> views,
|
@Default([]) List<ViewPB> views,
|
||||||
@Default([]) List<ChatInputActionPage> pages,
|
@Default([]) List<ChatInputMention> pages,
|
||||||
@Default([]) List<ChatInputActionPage> selectedPages,
|
@Default([]) List<ChatInputMention> selectedPages,
|
||||||
@Default("") String filter,
|
@Default("") String filter,
|
||||||
ChatInputKeyboardEvent? keyboardKey,
|
ChatInputKeyboardEvent? keyboardKey,
|
||||||
@Default(ChatActionMenuIndicator.loading())
|
@Default(ChatActionMenuIndicator.loading())
|
||||||
|
@ -5,14 +5,15 @@ import 'package:equatable/equatable.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
abstract class ChatInputActionPage extends Equatable {
|
abstract class ChatInputMention extends Equatable {
|
||||||
String get title;
|
String get title;
|
||||||
String get pageId;
|
String get pageId;
|
||||||
dynamic get page;
|
dynamic get page;
|
||||||
Widget get icon;
|
Widget get icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef ChatInputMetadata = Map<String, ChatInputActionPage>;
|
/// Key: the key is the pageId
|
||||||
|
typedef ChatInputMentionMetadata = Map<String, ChatInputMention>;
|
||||||
|
|
||||||
class ChatInputActionControl extends ChatActionHandler {
|
class ChatInputActionControl extends ChatActionHandler {
|
||||||
ChatInputActionControl({
|
ChatInputActionControl({
|
||||||
@ -35,9 +36,9 @@ class ChatInputActionControl extends ChatActionHandler {
|
|||||||
List<String> get tags =>
|
List<String> get tags =>
|
||||||
_commandBloc.state.selectedPages.map((e) => e.title).toList();
|
_commandBloc.state.selectedPages.map((e) => e.title).toList();
|
||||||
|
|
||||||
ChatInputMetadata consumeMetaData() {
|
ChatInputMentionMetadata consumeMetaData() {
|
||||||
final metadata = _commandBloc.state.selectedPages.fold(
|
final metadata = _commandBloc.state.selectedPages.fold(
|
||||||
<String, ChatInputActionPage>{},
|
<String, ChatInputMention>{},
|
||||||
(map, page) => map..putIfAbsent(page.pageId, () => page),
|
(map, page) => map..putIfAbsent(page.pageId, () => page),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -70,7 +71,7 @@ class ChatInputActionControl extends ChatActionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onSelected(ChatInputActionPage page) {
|
void onSelected(ChatInputMention page) {
|
||||||
_commandBloc.add(ChatInputActionEvent.addPage(page));
|
_commandBloc.add(ChatInputActionEvent.addPage(page));
|
||||||
textController.text = "$_showMenuText${page.title}";
|
textController.text = "$_showMenuText${page.title}";
|
||||||
|
|
||||||
|
@ -81,3 +81,7 @@ class AIType with _$AIType {
|
|||||||
const factory AIType.appflowyAI() = _AppFlowyAI;
|
const factory AIType.appflowyAI() = _AppFlowyAI;
|
||||||
const factory AIType.localAI() = _LocalAI;
|
const factory AIType.localAI() = _LocalAI;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension AITypeX on AIType {
|
||||||
|
bool isLocalAI() => this is _LocalAI;
|
||||||
|
}
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'chat_input_file_bloc.freezed.dart';
|
||||||
|
|
||||||
|
class ChatInputFileBloc extends Bloc<ChatInputFileEvent, ChatInputFileState> {
|
||||||
|
ChatInputFileBloc({
|
||||||
|
// ignore: avoid_unused_constructor_parameters
|
||||||
|
required String chatId,
|
||||||
|
required this.file,
|
||||||
|
}) : super(const ChatInputFileState()) {
|
||||||
|
on<ChatInputFileEvent>(
|
||||||
|
(event, emit) async {
|
||||||
|
await event.when(
|
||||||
|
initial: () async {},
|
||||||
|
updateUploadState: (UploadFileIndicator indicator) {
|
||||||
|
emit(state.copyWith(uploadFileIndicator: indicator));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ChatFile file;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ChatInputFileEvent with _$ChatInputFileEvent {
|
||||||
|
const factory ChatInputFileEvent.initial() = Initial;
|
||||||
|
const factory ChatInputFileEvent.updateUploadState(
|
||||||
|
UploadFileIndicator indicator,
|
||||||
|
) = _UpdateUploadState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ChatInputFileState with _$ChatInputFileState {
|
||||||
|
const factory ChatInputFileState({
|
||||||
|
UploadFileIndicator? uploadFileIndicator,
|
||||||
|
}) = _ChatInputFileState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class UploadFileIndicator with _$UploadFileIndicator {
|
||||||
|
const factory UploadFileIndicator.finish() = _Finish;
|
||||||
|
const factory UploadFileIndicator.uploading() = _Uploading;
|
||||||
|
const factory UploadFileIndicator.error(String error) = _Error;
|
||||||
|
}
|
@ -1,3 +1,6 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_input_action_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_input_action_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
@ -5,6 +8,97 @@ import 'package:appflowy_backend/log.dart';
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
|
import 'package:nanoid/nanoid.dart';
|
||||||
|
|
||||||
|
/// Indicate file source from appflowy document
|
||||||
|
const appflowySoruce = "appflowy";
|
||||||
|
|
||||||
|
List<ChatFile> fileListFromMessageMetadata(
|
||||||
|
Map<String, dynamic>? map,
|
||||||
|
) {
|
||||||
|
final List<ChatFile> metadata = [];
|
||||||
|
if (map != null) {
|
||||||
|
for (final entry in map.entries) {
|
||||||
|
if (entry.value is ChatFile) {
|
||||||
|
metadata.add(entry.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ChatFile> chatFilesFromMetadataString(String? s) {
|
||||||
|
if (s == null || s.isEmpty || s == "null") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final metadataJson = jsonDecode(s);
|
||||||
|
if (metadataJson is Map<String, dynamic>) {
|
||||||
|
final file = chatFileFromMap(metadataJson);
|
||||||
|
if (file != null) {
|
||||||
|
return [file];
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} else if (metadataJson is List) {
|
||||||
|
return metadataJson
|
||||||
|
.map((e) => e as Map<String, dynamic>)
|
||||||
|
.map(chatFileFromMap)
|
||||||
|
.where((file) => file != null)
|
||||||
|
.cast<ChatFile>()
|
||||||
|
.toList();
|
||||||
|
} else {
|
||||||
|
Log.error("Invalid metadata: $metadataJson");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatFile? chatFileFromMap(Map<String, dynamic>? map) {
|
||||||
|
if (map == null) return null;
|
||||||
|
|
||||||
|
final filePath = map['source'] as String?;
|
||||||
|
final fileName = map['name'] as String?;
|
||||||
|
|
||||||
|
if (filePath == null || fileName == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ChatFile.fromFilePath(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ChatMessageRefSource> messageReferenceSource(String? s) {
|
||||||
|
if (s == null || s.isEmpty || s == "null") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<ChatMessageRefSource> metadata = [];
|
||||||
|
try {
|
||||||
|
final metadataJson = jsonDecode(s);
|
||||||
|
if (metadataJson == null) {
|
||||||
|
Log.warn("metadata is null");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// [{"id":null,"name":"The Five Dysfunctions of a Team.pdf","source":"/Users/weidongfu/Desktop/The Five Dysfunctions of a Team.pdf"}]
|
||||||
|
|
||||||
|
if (metadataJson is Map<String, dynamic>) {
|
||||||
|
if (metadataJson.isNotEmpty) {
|
||||||
|
metadata.add(ChatMessageRefSource.fromJson(metadataJson));
|
||||||
|
}
|
||||||
|
} else if (metadataJson is List) {
|
||||||
|
metadata.addAll(
|
||||||
|
metadataJson.map(
|
||||||
|
(e) => ChatMessageRefSource.fromJson(e as Map<String, dynamic>),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Log.error("Invalid metadata: $metadataJson");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error("Failed to parse metadata: $e");
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<ChatMessageMetaPB>> metadataPBFromMetadata(
|
Future<List<ChatMessageMetaPB>> metadataPBFromMetadata(
|
||||||
Map<String, dynamic>? map,
|
Map<String, dynamic>? map,
|
||||||
@ -24,7 +118,8 @@ Future<List<ChatMessageMetaPB>> metadataPBFromMetadata(
|
|||||||
id: view.id,
|
id: view.id,
|
||||||
name: view.name,
|
name: view.name,
|
||||||
data: pb.text,
|
data: pb.text,
|
||||||
source: "appflowy document",
|
dataType: ChatMessageMetaTypePB.Txt,
|
||||||
|
source: appflowySoruce,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}, (err) {
|
}, (err) {
|
||||||
@ -32,6 +127,31 @@ Future<List<ChatMessageMetaPB>> metadataPBFromMetadata(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (entry.value is ChatFile) {
|
||||||
|
metadata.add(
|
||||||
|
ChatMessageMetaPB(
|
||||||
|
id: nanoid(8),
|
||||||
|
name: entry.value.fileName,
|
||||||
|
data: entry.value.filePath,
|
||||||
|
dataType: entry.value.fileType,
|
||||||
|
source: entry.value.filePath,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ChatFile> chatFilesFromMessageMetadata(
|
||||||
|
Map<String, dynamic>? map,
|
||||||
|
) {
|
||||||
|
final List<ChatFile> metadata = [];
|
||||||
|
if (map != null) {
|
||||||
|
for (final entry in map.entries) {
|
||||||
|
if (entry.value is ChatFile) {
|
||||||
|
metadata.add(entry.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,193 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:ffi';
|
||||||
|
import 'dart:isolate';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_message_service.dart';
|
||||||
|
|
||||||
|
class AnswerStream {
|
||||||
|
AnswerStream() {
|
||||||
|
_port.handler = _controller.add;
|
||||||
|
_subscription = _controller.stream.listen(
|
||||||
|
(event) {
|
||||||
|
if (event.startsWith("data:")) {
|
||||||
|
_hasStarted = true;
|
||||||
|
final newText = event.substring(5);
|
||||||
|
_text += newText;
|
||||||
|
if (_onData != null) {
|
||||||
|
_onData!(_text);
|
||||||
|
}
|
||||||
|
} else if (event.startsWith("error:")) {
|
||||||
|
_error = event.substring(5);
|
||||||
|
if (_onError != null) {
|
||||||
|
_onError!(_error!);
|
||||||
|
}
|
||||||
|
} else if (event.startsWith("metadata:")) {
|
||||||
|
if (_onMetadata != null) {
|
||||||
|
final s = event.substring(9);
|
||||||
|
_onMetadata!(messageReferenceSource(s));
|
||||||
|
}
|
||||||
|
} else if (event == "AI_RESPONSE_LIMIT") {
|
||||||
|
if (_onAIResponseLimit != null) {
|
||||||
|
_onAIResponseLimit!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: () {
|
||||||
|
if (_onEnd != null) {
|
||||||
|
_onEnd!();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) {
|
||||||
|
if (_onError != null) {
|
||||||
|
_onError!(error.toString());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final RawReceivePort _port = RawReceivePort();
|
||||||
|
final StreamController<String> _controller = StreamController.broadcast();
|
||||||
|
late StreamSubscription<String> _subscription;
|
||||||
|
bool _hasStarted = false;
|
||||||
|
String? _error;
|
||||||
|
String _text = "";
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
void Function(String text)? _onData;
|
||||||
|
void Function()? _onStart;
|
||||||
|
void Function()? _onEnd;
|
||||||
|
void Function(String error)? _onError;
|
||||||
|
void Function()? _onAIResponseLimit;
|
||||||
|
void Function(List<ChatMessageRefSource> metadata)? _onMetadata;
|
||||||
|
|
||||||
|
int get nativePort => _port.sendPort.nativePort;
|
||||||
|
bool get hasStarted => _hasStarted;
|
||||||
|
String? get error => _error;
|
||||||
|
String get text => _text;
|
||||||
|
|
||||||
|
Future<void> dispose() async {
|
||||||
|
await _controller.close();
|
||||||
|
await _subscription.cancel();
|
||||||
|
_port.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void listen({
|
||||||
|
void Function(String text)? onData,
|
||||||
|
void Function()? onStart,
|
||||||
|
void Function()? onEnd,
|
||||||
|
void Function(String error)? onError,
|
||||||
|
void Function()? onAIResponseLimit,
|
||||||
|
void Function(List<ChatMessageRefSource> metadata)? onMetadata,
|
||||||
|
}) {
|
||||||
|
_onData = onData;
|
||||||
|
_onStart = onStart;
|
||||||
|
_onEnd = onEnd;
|
||||||
|
_onError = onError;
|
||||||
|
_onAIResponseLimit = onAIResponseLimit;
|
||||||
|
_onMetadata = onMetadata;
|
||||||
|
|
||||||
|
if (_onStart != null) {
|
||||||
|
_onStart!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class QuestionStream {
|
||||||
|
QuestionStream() {
|
||||||
|
_port.handler = _controller.add;
|
||||||
|
_subscription = _controller.stream.listen(
|
||||||
|
(event) {
|
||||||
|
if (event.startsWith("data:")) {
|
||||||
|
_hasStarted = true;
|
||||||
|
final newText = event.substring(5);
|
||||||
|
_text += newText;
|
||||||
|
if (_onData != null) {
|
||||||
|
_onData!(_text);
|
||||||
|
}
|
||||||
|
} else if (event.startsWith("message_id:")) {
|
||||||
|
final messageId = event.substring(11);
|
||||||
|
_onMessageId?.call(messageId);
|
||||||
|
} else if (event.startsWith("start_index_file:")) {
|
||||||
|
final indexName = event.substring(17);
|
||||||
|
_onFileIndexStart?.call(indexName);
|
||||||
|
} else if (event.startsWith("end_index_file:")) {
|
||||||
|
final indexName = event.substring(10);
|
||||||
|
_onFileIndexEnd?.call(indexName);
|
||||||
|
} else if (event.startsWith("index_file_error:")) {
|
||||||
|
final indexName = event.substring(16);
|
||||||
|
_onFileIndexError?.call(indexName);
|
||||||
|
} else if (event.startsWith("index_start:")) {
|
||||||
|
_onIndexStart?.call();
|
||||||
|
} else if (event.startsWith("index_end:")) {
|
||||||
|
_onIndexEnd?.call();
|
||||||
|
} else if (event.startsWith("done:")) {
|
||||||
|
_onDone?.call();
|
||||||
|
} else if (event.startsWith("error:")) {
|
||||||
|
_error = event.substring(5);
|
||||||
|
if (_onError != null) {
|
||||||
|
_onError!(_error!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) {
|
||||||
|
if (_onError != null) {
|
||||||
|
_onError!(error.toString());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final RawReceivePort _port = RawReceivePort();
|
||||||
|
final StreamController<String> _controller = StreamController.broadcast();
|
||||||
|
late StreamSubscription<String> _subscription;
|
||||||
|
bool _hasStarted = false;
|
||||||
|
String? _error;
|
||||||
|
String _text = "";
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
void Function(String text)? _onData;
|
||||||
|
void Function(String error)? _onError;
|
||||||
|
void Function(String messageId)? _onMessageId;
|
||||||
|
void Function(String indexName)? _onFileIndexStart;
|
||||||
|
void Function(String indexName)? _onFileIndexEnd;
|
||||||
|
void Function(String indexName)? _onFileIndexError;
|
||||||
|
void Function()? _onIndexStart;
|
||||||
|
void Function()? _onIndexEnd;
|
||||||
|
void Function()? _onDone;
|
||||||
|
|
||||||
|
int get nativePort => _port.sendPort.nativePort;
|
||||||
|
bool get hasStarted => _hasStarted;
|
||||||
|
String? get error => _error;
|
||||||
|
String get text => _text;
|
||||||
|
|
||||||
|
Future<void> dispose() async {
|
||||||
|
await _controller.close();
|
||||||
|
await _subscription.cancel();
|
||||||
|
_port.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void listen({
|
||||||
|
void Function(String text)? onData,
|
||||||
|
void Function(String error)? onError,
|
||||||
|
void Function(String messageId)? onMessageId,
|
||||||
|
void Function(String indexName)? onFileIndexStart,
|
||||||
|
void Function(String indexName)? onFileIndexEnd,
|
||||||
|
void Function(String indexName)? onFileIndexFail,
|
||||||
|
void Function()? onIndexStart,
|
||||||
|
void Function()? onIndexEnd,
|
||||||
|
void Function()? onDone,
|
||||||
|
}) {
|
||||||
|
_onData = onData;
|
||||||
|
_onError = onError;
|
||||||
|
_onMessageId = onMessageId;
|
||||||
|
|
||||||
|
_onFileIndexStart = onFileIndexStart;
|
||||||
|
_onFileIndexEnd = onFileIndexEnd;
|
||||||
|
_onFileIndexError = onFileIndexFail;
|
||||||
|
|
||||||
|
_onIndexStart = onIndexStart;
|
||||||
|
_onIndexEnd = onIndexEnd;
|
||||||
|
_onDone = onDone;
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
@ -19,7 +19,7 @@ class ChatSidePannelBloc
|
|||||||
on<ChatSidePannelEvent>(
|
on<ChatSidePannelEvent>(
|
||||||
(event, emit) async {
|
(event, emit) async {
|
||||||
await event.when(
|
await event.when(
|
||||||
selectedMetadata: (ChatMessageMetadata metadata) async {
|
selectedMetadata: (ChatMessageRefSource metadata) async {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
@ -62,7 +62,7 @@ class ChatSidePannelBloc
|
|||||||
@freezed
|
@freezed
|
||||||
class ChatSidePannelEvent with _$ChatSidePannelEvent {
|
class ChatSidePannelEvent with _$ChatSidePannelEvent {
|
||||||
const factory ChatSidePannelEvent.selectedMetadata(
|
const factory ChatSidePannelEvent.selectedMetadata(
|
||||||
ChatMessageMetadata metadata,
|
ChatMessageRefSource metadata,
|
||||||
) = _SelectedMetadata;
|
) = _SelectedMetadata;
|
||||||
const factory ChatSidePannelEvent.close() = _Close;
|
const factory ChatSidePannelEvent.close() = _Close;
|
||||||
const factory ChatSidePannelEvent.open(ViewPB view) = _Open;
|
const factory ChatSidePannelEvent.open(ViewPB view) = _Open;
|
||||||
@ -71,7 +71,7 @@ class ChatSidePannelEvent with _$ChatSidePannelEvent {
|
|||||||
@freezed
|
@freezed
|
||||||
class ChatSidePannelState with _$ChatSidePannelState {
|
class ChatSidePannelState with _$ChatSidePannelState {
|
||||||
const factory ChatSidePannelState({
|
const factory ChatSidePannelState({
|
||||||
ChatMessageMetadata? metadata,
|
ChatMessageRefSource? metadata,
|
||||||
@Default(ChatSidePannelIndicator.loading())
|
@Default(ChatSidePannelIndicator.loading())
|
||||||
ChatSidePannelIndicator indicator,
|
ChatSidePannelIndicator indicator,
|
||||||
@Default(false) bool isShowPannel,
|
@Default(false) bool isShowPannel,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:appflowy/plugins/ai_chat/application/chat_member_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
part 'chat_user_message_bloc.freezed.dart';
|
part 'chat_user_message_bloc.freezed.dart';
|
||||||
@ -8,15 +8,85 @@ part 'chat_user_message_bloc.freezed.dart';
|
|||||||
class ChatUserMessageBloc
|
class ChatUserMessageBloc
|
||||||
extends Bloc<ChatUserMessageEvent, ChatUserMessageState> {
|
extends Bloc<ChatUserMessageEvent, ChatUserMessageState> {
|
||||||
ChatUserMessageBloc({
|
ChatUserMessageBloc({
|
||||||
required Message message,
|
required dynamic message,
|
||||||
required ChatMember? member,
|
}) : super(
|
||||||
}) : super(ChatUserMessageState.initial(message, member)) {
|
ChatUserMessageState.initial(
|
||||||
|
message,
|
||||||
|
),
|
||||||
|
) {
|
||||||
on<ChatUserMessageEvent>(
|
on<ChatUserMessageEvent>(
|
||||||
(event, emit) async {
|
(event, emit) {
|
||||||
event.when(
|
event.when(
|
||||||
initial: () {},
|
initial: () {
|
||||||
refreshMember: (ChatMember member) {
|
if (state.stream != null) {
|
||||||
emit(state.copyWith(member: member));
|
if (!isClosed) {
|
||||||
|
add(ChatUserMessageEvent.updateText(state.stream!.text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.stream?.listen(
|
||||||
|
onData: (text) {
|
||||||
|
if (!isClosed) {
|
||||||
|
add(ChatUserMessageEvent.updateText(text));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMessageId: (messageId) {
|
||||||
|
if (!isClosed) {
|
||||||
|
add(ChatUserMessageEvent.updateMessageId(messageId));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) {
|
||||||
|
if (!isClosed) {
|
||||||
|
add(ChatUserMessageEvent.receiveError(error.toString()));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFileIndexStart: (indexName) {
|
||||||
|
Log.debug("index start: $indexName");
|
||||||
|
},
|
||||||
|
onFileIndexEnd: (indexName) {
|
||||||
|
Log.info("index end: $indexName");
|
||||||
|
},
|
||||||
|
onFileIndexFail: (indexName) {
|
||||||
|
Log.debug("index fail: $indexName");
|
||||||
|
},
|
||||||
|
onIndexStart: () {
|
||||||
|
if (!isClosed) {
|
||||||
|
add(
|
||||||
|
const ChatUserMessageEvent.updateQuestionState(
|
||||||
|
QuestionMessageState.indexStart(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onIndexEnd: () {
|
||||||
|
if (!isClosed) {
|
||||||
|
add(
|
||||||
|
const ChatUserMessageEvent.updateQuestionState(
|
||||||
|
QuestionMessageState.indexEnd(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: () {
|
||||||
|
if (!isClosed) {
|
||||||
|
add(
|
||||||
|
const ChatUserMessageEvent.updateQuestionState(
|
||||||
|
QuestionMessageState.finish(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
updateText: (String text) {
|
||||||
|
emit(state.copyWith(text: text));
|
||||||
|
},
|
||||||
|
updateMessageId: (String messageId) {
|
||||||
|
emit(state.copyWith(messageId: messageId));
|
||||||
|
},
|
||||||
|
receiveError: (String error) {},
|
||||||
|
updateQuestionState: (QuestionMessageState newState) {
|
||||||
|
emit(state.copyWith(messageState: newState));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -27,20 +97,47 @@ class ChatUserMessageBloc
|
|||||||
@freezed
|
@freezed
|
||||||
class ChatUserMessageEvent with _$ChatUserMessageEvent {
|
class ChatUserMessageEvent with _$ChatUserMessageEvent {
|
||||||
const factory ChatUserMessageEvent.initial() = Initial;
|
const factory ChatUserMessageEvent.initial() = Initial;
|
||||||
const factory ChatUserMessageEvent.refreshMember(ChatMember member) =
|
const factory ChatUserMessageEvent.updateText(String text) = _UpdateText;
|
||||||
_MemberInfo;
|
const factory ChatUserMessageEvent.updateQuestionState(
|
||||||
|
QuestionMessageState newState,
|
||||||
|
) = _UpdateQuestionState;
|
||||||
|
const factory ChatUserMessageEvent.updateMessageId(String messageId) =
|
||||||
|
_UpdateMessageId;
|
||||||
|
const factory ChatUserMessageEvent.receiveError(String error) = _ReceiveError;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class ChatUserMessageState with _$ChatUserMessageState {
|
class ChatUserMessageState with _$ChatUserMessageState {
|
||||||
const factory ChatUserMessageState({
|
const factory ChatUserMessageState({
|
||||||
required Message message,
|
required String text,
|
||||||
ChatMember? member,
|
QuestionStream? stream,
|
||||||
|
String? messageId,
|
||||||
|
@Default(QuestionMessageState.finish()) QuestionMessageState messageState,
|
||||||
}) = _ChatUserMessageState;
|
}) = _ChatUserMessageState;
|
||||||
|
|
||||||
factory ChatUserMessageState.initial(
|
factory ChatUserMessageState.initial(
|
||||||
Message message,
|
dynamic message,
|
||||||
ChatMember? member,
|
|
||||||
) =>
|
) =>
|
||||||
ChatUserMessageState(message: message, member: member);
|
ChatUserMessageState(
|
||||||
|
text: message is String ? message : "",
|
||||||
|
stream: message is QuestionStream ? message : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class QuestionMessageState with _$QuestionMessageState {
|
||||||
|
const factory QuestionMessageState.indexFileStart(String fileName) =
|
||||||
|
_IndexFileStart;
|
||||||
|
const factory QuestionMessageState.indexFileEnd(String fileName) =
|
||||||
|
_IndexFileEnd;
|
||||||
|
const factory QuestionMessageState.indexFileFail(String fileName) =
|
||||||
|
_IndexFileFail;
|
||||||
|
|
||||||
|
const factory QuestionMessageState.indexStart() = _IndexStart;
|
||||||
|
const factory QuestionMessageState.indexEnd() = _IndexEnd;
|
||||||
|
const factory QuestionMessageState.finish() = _Finish;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension QuestionMessageStateX on QuestionMessageState {
|
||||||
|
bool get isFinish => this is _Finish;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
import 'chat_message_service.dart';
|
||||||
|
|
||||||
|
part 'chat_user_message_bubble_bloc.freezed.dart';
|
||||||
|
|
||||||
|
class ChatUserMessageBubbleBloc
|
||||||
|
extends Bloc<ChatUserMessageBubbleEvent, ChatUserMessageBubbleState> {
|
||||||
|
ChatUserMessageBubbleBloc({
|
||||||
|
required Message message,
|
||||||
|
}) : super(
|
||||||
|
ChatUserMessageBubbleState.initial(
|
||||||
|
message,
|
||||||
|
_getFiles(message.metadata),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
on<ChatUserMessageBubbleEvent>(
|
||||||
|
(event, emit) async {
|
||||||
|
event.when(
|
||||||
|
initial: () {},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ChatFile> _getFiles(Map<String, dynamic>? metadata) {
|
||||||
|
if (metadata == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
final refSourceMetadata = metadata[messageRefSourceJsonStringKey] as String?;
|
||||||
|
final files = metadata[messageChatFileListKey] as List<ChatFile>?;
|
||||||
|
|
||||||
|
if (refSourceMetadata != null) {
|
||||||
|
return chatFilesFromMetadataString(refSourceMetadata);
|
||||||
|
}
|
||||||
|
return files ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ChatUserMessageBubbleEvent with _$ChatUserMessageBubbleEvent {
|
||||||
|
const factory ChatUserMessageBubbleEvent.initial() = Initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ChatUserMessageBubbleState with _$ChatUserMessageBubbleState {
|
||||||
|
const factory ChatUserMessageBubbleState({
|
||||||
|
required Message message,
|
||||||
|
required List<ChatFile> files,
|
||||||
|
}) = _ChatUserMessageBubbleState;
|
||||||
|
|
||||||
|
factory ChatUserMessageBubbleState.initial(
|
||||||
|
Message message,
|
||||||
|
List<ChatFile> files,
|
||||||
|
) =>
|
||||||
|
ChatUserMessageBubbleState(message: message, files: files);
|
||||||
|
}
|
@ -2,15 +2,14 @@ import 'dart:math';
|
|||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_input_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_input_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_related_question.dart';
|
import 'package:appflowy/plugins/ai_chat/presentation/chat_related_question.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/presentation/message/ai_message_bubble.dart';
|
import 'package:appflowy/plugins/ai_chat/presentation/message/ai_message_bubble.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/presentation/message/other_user_message_bubble.dart';
|
import 'package:appflowy/plugins/ai_chat/presentation/message/other_user_message_bubble.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/presentation/message/user_message_bubble.dart';
|
import 'package:appflowy/plugins/ai_chat/presentation/message/user_message_bubble.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
import 'package:desktop_drop/desktop_drop.dart';
|
import 'package:desktop_drop/desktop_drop.dart';
|
||||||
@ -19,7 +18,6 @@ import 'package:flowy_infra/platform_extension.dart';
|
|||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
||||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||||
@ -29,7 +27,6 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
import 'application/chat_member_bloc.dart';
|
import 'application/chat_member_bloc.dart';
|
||||||
import 'application/chat_side_pannel_bloc.dart';
|
import 'application/chat_side_pannel_bloc.dart';
|
||||||
import 'presentation/chat_input/chat_input.dart';
|
import 'presentation/chat_input/chat_input.dart';
|
||||||
import 'presentation/chat_popmenu.dart';
|
|
||||||
import 'presentation/chat_side_pannel.dart';
|
import 'presentation/chat_side_pannel.dart';
|
||||||
import 'presentation/chat_theme.dart';
|
import 'presentation/chat_theme.dart';
|
||||||
import 'presentation/chat_user_invalid_message.dart';
|
import 'presentation/chat_user_invalid_message.dart';
|
||||||
@ -88,8 +85,7 @@ class AIChatPage extends StatelessWidget {
|
|||||||
|
|
||||||
/// [ChatFileBloc] is used to handle file indexing as a chat context
|
/// [ChatFileBloc] is used to handle file indexing as a chat context
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (_) => ChatFileBloc(chatId: view.id)
|
create: (_) => ChatFileBloc()..add(const ChatFileEvent.initial()),
|
||||||
..add(const ChatFileEvent.initial()),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
/// [ChatInputStateBloc] is used to handle chat input text field state
|
/// [ChatInputStateBloc] is used to handle chat input text field state
|
||||||
@ -100,40 +96,24 @@ class AIChatPage extends StatelessWidget {
|
|||||||
BlocProvider(create: (_) => ChatSidePannelBloc(chatId: view.id)),
|
BlocProvider(create: (_) => ChatSidePannelBloc(chatId: view.id)),
|
||||||
BlocProvider(create: (_) => ChatMemberBloc()),
|
BlocProvider(create: (_) => ChatMemberBloc()),
|
||||||
],
|
],
|
||||||
child: BlocListener<ChatFileBloc, ChatFileState>(
|
child: BlocBuilder<ChatFileBloc, ChatFileState>(
|
||||||
listenWhen: (previous, current) =>
|
builder: (context, state) {
|
||||||
previous.uploadFileIndicator != current.uploadFileIndicator,
|
return DropTarget(
|
||||||
listener: (context, state) {
|
onDragDone: (DropDoneDetails detail) async {
|
||||||
_handleIndexIndicator(state.uploadFileIndicator, context);
|
if (state.supportChatWithFile) {
|
||||||
},
|
for (final file in detail.files) {
|
||||||
child: BlocBuilder<ChatFileBloc, ChatFileState>(
|
context
|
||||||
builder: (context, state) {
|
.read<ChatFileBloc>()
|
||||||
return DropTarget(
|
.add(ChatFileEvent.newFile(file.path, file.name));
|
||||||
onDragDone: (DropDoneDetails detail) async {
|
|
||||||
if (state.supportChatWithFile) {
|
|
||||||
await showConfirmDialog(
|
|
||||||
context: context,
|
|
||||||
style: ConfirmPopupStyle.cancelAndOk,
|
|
||||||
title: LocaleKeys.chat_chatWithFilePrompt.tr(),
|
|
||||||
confirmLabel: LocaleKeys.button_confirm.tr(),
|
|
||||||
onConfirm: () {
|
|
||||||
for (final file in detail.files) {
|
|
||||||
context
|
|
||||||
.read<ChatFileBloc>()
|
|
||||||
.add(ChatFileEvent.newFile(file.path, file.name));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
description: '',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
child: _ChatContentPage(
|
},
|
||||||
view: view,
|
child: _ChatContentPage(
|
||||||
userProfile: userProfile,
|
view: view,
|
||||||
),
|
userProfile: userProfile,
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -145,35 +125,6 @@ class AIChatPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleIndexIndicator(
|
|
||||||
UploadFileIndicator? indicator,
|
|
||||||
BuildContext context,
|
|
||||||
) {
|
|
||||||
if (indicator != null) {
|
|
||||||
indicator.when(
|
|
||||||
finish: (fileName) {
|
|
||||||
showSnackBarMessage(
|
|
||||||
context,
|
|
||||||
LocaleKeys.chat_indexFileSuccess.tr(args: [fileName]),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
uploading: (fileName) {
|
|
||||||
showSnackBarMessage(
|
|
||||||
context,
|
|
||||||
LocaleKeys.chat_indexingFile.tr(args: [fileName]),
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
error: (err) {
|
|
||||||
showSnackBarMessage(
|
|
||||||
context,
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChatContentPage extends StatefulWidget {
|
class _ChatContentPage extends StatefulWidget {
|
||||||
@ -302,31 +253,31 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
|||||||
// We use custom bottom widget for chat input, so
|
// We use custom bottom widget for chat input, so
|
||||||
// do not need to handle this event.
|
// do not need to handle this event.
|
||||||
},
|
},
|
||||||
customBottomWidget: buildBottom(blocContext),
|
customBottomWidget: _buildBottom(blocContext),
|
||||||
user: _user,
|
user: _user,
|
||||||
theme: buildTheme(context),
|
theme: buildTheme(context),
|
||||||
onEndReached: () async {
|
onEndReached: () async {
|
||||||
if (state.hasMorePrevMessage &&
|
if (state.hasMorePrevMessage &&
|
||||||
state.loadingPreviousStatus != const LoadingState.loading()) {
|
state.loadingPreviousStatus.isFinish) {
|
||||||
blocContext
|
blocContext
|
||||||
.read<ChatBloc>()
|
.read<ChatBloc>()
|
||||||
.add(const ChatEvent.startLoadingPrevMessage());
|
.add(const ChatEvent.startLoadingPrevMessage());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emptyState: BlocBuilder<ChatBloc, ChatState>(
|
emptyState: BlocBuilder<ChatBloc, ChatState>(
|
||||||
builder: (_, state) =>
|
builder: (_, state) => state.initialLoadingStatus.isFinish
|
||||||
state.initialLoadingStatus == const LoadingState.finish()
|
? Padding(
|
||||||
? Padding(
|
padding: AIChatUILayout.welcomePagePadding,
|
||||||
padding: AIChatUILayout.welcomePagePadding,
|
child: ChatWelcomePage(
|
||||||
child: ChatWelcomePage(
|
userProfile: widget.userProfile,
|
||||||
onSelectedQuestion: (question) => blocContext
|
onSelectedQuestion: (question) => blocContext
|
||||||
.read<ChatBloc>()
|
.read<ChatBloc>()
|
||||||
.add(ChatEvent.sendMessage(message: question)),
|
.add(ChatEvent.sendMessage(message: question)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const Center(
|
: const Center(
|
||||||
child: CircularProgressIndicator.adaptive(),
|
child: CircularProgressIndicator.adaptive(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
messageWidthRatio: AIChatUILayout.messageWidthRatio,
|
messageWidthRatio: AIChatUILayout.messageWidthRatio,
|
||||||
textMessageBuilder: (
|
textMessageBuilder: (
|
||||||
@ -339,45 +290,55 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
|||||||
child, {
|
child, {
|
||||||
required message,
|
required message,
|
||||||
required nextMessageInGroup,
|
required nextMessageInGroup,
|
||||||
}) {
|
}) =>
|
||||||
if (message.author.id == _user.id) {
|
_buildBubble(blocContext, message, child, state),
|
||||||
return ChatUserMessageBubble(
|
|
||||||
message: message,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
} else if (isOtherUserMessage(message)) {
|
|
||||||
return OtherUserMessageBubble(
|
|
||||||
message: message,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return _buildAIBubble(message, blocContext, state, child);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildBubble(
|
||||||
|
BuildContext blocContext,
|
||||||
|
Message message,
|
||||||
|
Widget child,
|
||||||
|
ChatState state,
|
||||||
|
) {
|
||||||
|
if (message.author.id == _user.id) {
|
||||||
|
return ChatUserMessageBubble(
|
||||||
|
message: message,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
} else if (isOtherUserMessage(message)) {
|
||||||
|
return OtherUserMessageBubble(
|
||||||
|
message: message,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return _buildAIBubble(message, blocContext, state, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildTextMessage(BuildContext context, TextMessage message) {
|
Widget _buildTextMessage(BuildContext context, TextMessage message) {
|
||||||
if (message.author.id == _user.id) {
|
if (message.author.id == _user.id) {
|
||||||
return ChatTextMessageWidget(
|
final stream = message.metadata?["$QuestionStream"];
|
||||||
|
return ChatUserMessageWidget(
|
||||||
|
key: ValueKey(message.id),
|
||||||
user: message.author,
|
user: message.author,
|
||||||
messageUserId: message.id,
|
message: stream is QuestionStream ? stream : message.text,
|
||||||
text: message.text,
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final stream = message.metadata?["$AnswerStream"];
|
final stream = message.metadata?["$AnswerStream"];
|
||||||
final questionId = message.metadata?["question"];
|
final questionId = message.metadata?[messageQuestionIdKey];
|
||||||
final metadata = message.metadata?["metadata"] as String?;
|
final refSourceJsonString =
|
||||||
return ChatAITextMessageWidget(
|
message.metadata?[messageRefSourceJsonStringKey] as String?;
|
||||||
|
return ChatAIMessageWidget(
|
||||||
user: message.author,
|
user: message.author,
|
||||||
messageUserId: message.id,
|
messageUserId: message.id,
|
||||||
text: stream is AnswerStream ? stream : message.text,
|
message: stream is AnswerStream ? stream : message.text,
|
||||||
key: ValueKey(message.id),
|
key: ValueKey(message.id),
|
||||||
questionId: questionId,
|
questionId: questionId,
|
||||||
chatId: widget.view.id,
|
chatId: widget.view.id,
|
||||||
metadata: metadata,
|
refSourceJsonString: refSourceJsonString,
|
||||||
onSelectedMetadata: (ChatMessageMetadata metadata) {
|
onSelectedMetadata: (ChatMessageRefSource metadata) {
|
||||||
context.read<ChatSidePannelBloc>().add(
|
context.read<ChatSidePannelBloc>().add(
|
||||||
ChatSidePannelEvent.selectedMetadata(metadata),
|
ChatSidePannelEvent.selectedMetadata(metadata),
|
||||||
);
|
);
|
||||||
@ -424,68 +385,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildBubble(Message message, Widget child) {
|
Widget _buildBottom(BuildContext context) {
|
||||||
final isAuthor = message.author.id == _user.id;
|
|
||||||
const borderRadius = BorderRadius.all(Radius.circular(6));
|
|
||||||
final childWithPadding = isAuthor
|
|
||||||
? Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
|
||||||
child: child,
|
|
||||||
)
|
|
||||||
: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
|
|
||||||
// If the message is from the author, we will decorate it with a different color
|
|
||||||
final decoratedChild = isAuthor
|
|
||||||
? DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: borderRadius,
|
|
||||||
color: !isAuthor || message.type == types.MessageType.image
|
|
||||||
? AFThemeExtension.of(context).tint1
|
|
||||||
: Theme.of(context).colorScheme.secondary,
|
|
||||||
),
|
|
||||||
child: childWithPadding,
|
|
||||||
)
|
|
||||||
: childWithPadding;
|
|
||||||
|
|
||||||
// If the message is from the author, no further actions are needed
|
|
||||||
if (isAuthor) {
|
|
||||||
return ClipRRect(
|
|
||||||
borderRadius: borderRadius,
|
|
||||||
child: decoratedChild,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (isMobile) {
|
|
||||||
return ChatPopupMenu(
|
|
||||||
onAction: (action) {
|
|
||||||
switch (action) {
|
|
||||||
case ChatMessageAction.copy:
|
|
||||||
if (message is TextMessage) {
|
|
||||||
Clipboard.setData(ClipboardData(text: message.text));
|
|
||||||
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
builder: (context) =>
|
|
||||||
ClipRRect(borderRadius: borderRadius, child: decoratedChild),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Show hover effect only on desktop
|
|
||||||
return ClipRRect(
|
|
||||||
borderRadius: borderRadius,
|
|
||||||
child: ChatAIMessageHover(
|
|
||||||
message: message,
|
|
||||||
child: decoratedChild,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildBottom(BuildContext context) {
|
|
||||||
return ClipRect(
|
return ClipRect(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: AIChatUILayout.safeAreaInsets(context),
|
padding: AIChatUILayout.safeAreaInsets(context),
|
||||||
|
@ -17,10 +17,11 @@ class ChatInputAtButton extends StatelessWidget {
|
|||||||
message: LocaleKeys.chat_clickToMention.tr(),
|
message: LocaleKeys.chat_clickToMention.tr(),
|
||||||
child: FlowyIconButton(
|
child: FlowyIconButton(
|
||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
radius: BorderRadius.circular(18),
|
radius: BorderRadius.circular(6),
|
||||||
icon: const FlowySvg(
|
icon: FlowySvg(
|
||||||
FlowySvgs.mention_s,
|
FlowySvgs.chat_at_s,
|
||||||
size: Size.square(20),
|
size: const Size.square(20),
|
||||||
|
color: Colors.grey.shade600,
|
||||||
),
|
),
|
||||||
onPressed: onTap,
|
onPressed: onTap,
|
||||||
),
|
),
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_input_action_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_input_action_bloc.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_input_action_control.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_input_action_control.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_input_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_input_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/ai_chat/presentation/chat_input/chat_input_file.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_input_action_menu.dart';
|
import 'package:appflowy/plugins/ai_chat/presentation/chat_input_action_menu.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
@ -18,9 +20,10 @@ import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
|||||||
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
|
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
|
||||||
|
|
||||||
import 'chat_at_button.dart';
|
import 'chat_at_button.dart';
|
||||||
import 'chat_attachment.dart';
|
import 'chat_input_attachment.dart';
|
||||||
import 'chat_input_span.dart';
|
|
||||||
import 'chat_send_button.dart';
|
import 'chat_send_button.dart';
|
||||||
|
import 'chat_input_span.dart';
|
||||||
|
import 'layout_define.dart';
|
||||||
|
|
||||||
class ChatInput extends StatefulWidget {
|
class ChatInput extends StatefulWidget {
|
||||||
/// Creates [ChatInput] widget.
|
/// Creates [ChatInput] widget.
|
||||||
@ -105,16 +108,6 @@ class _ChatInputState extends State<ChatInput> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const buttonPadding = EdgeInsets.symmetric(horizontal: 2);
|
|
||||||
const inputPadding = EdgeInsets.all(6);
|
|
||||||
final textPadding = isMobile
|
|
||||||
? const EdgeInsets.only(left: 8.0, right: 4.0)
|
|
||||||
: const EdgeInsets.symmetric(horizontal: 16);
|
|
||||||
final borderRadius = BorderRadius.circular(isMobile ? 10 : 30);
|
|
||||||
final color = isMobile
|
|
||||||
? Colors.transparent
|
|
||||||
: Theme.of(context).colorScheme.surfaceContainerHighest;
|
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: inputPadding,
|
padding: inputPadding,
|
||||||
// ignore: use_decorated_box
|
// ignore: use_decorated_box
|
||||||
@ -123,7 +116,7 @@ class _ChatInputState extends State<ChatInput> {
|
|||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: _inputFocusNode.hasFocus && !isMobile
|
color: _inputFocusNode.hasFocus && !isMobile
|
||||||
? Theme.of(context).colorScheme.primary.withOpacity(0.6)
|
? Theme.of(context).colorScheme.primary.withOpacity(0.6)
|
||||||
: Colors.transparent,
|
: Theme.of(context).colorScheme.secondary,
|
||||||
),
|
),
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
),
|
),
|
||||||
@ -132,17 +125,50 @@ class _ChatInputState extends State<ChatInput> {
|
|||||||
color: color,
|
color: color,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
child: Row(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// TODO(lucas): support mobile
|
if (context.read<ChatFileBloc>().state.uploadFiles.isNotEmpty)
|
||||||
if (PlatformExtension.isDesktop &&
|
Padding(
|
||||||
widget.aiType == const AIType.localAI())
|
padding: EdgeInsets.only(
|
||||||
_attachmentButton(buttonPadding),
|
top: 12,
|
||||||
Expanded(child: _inputTextField(context, textPadding)),
|
bottom: 12,
|
||||||
|
left: textPadding.left + sendButtonSize,
|
||||||
|
right: textPadding.right,
|
||||||
|
),
|
||||||
|
child: BlocBuilder<ChatFileBloc, ChatFileState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return ChatInputFile(
|
||||||
|
chatId: widget.chatId,
|
||||||
|
files: state.uploadFiles,
|
||||||
|
onDeleted: (file) => context.read<ChatFileBloc>().add(
|
||||||
|
ChatFileEvent.deleteFile(file),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
if (widget.aiType == const AIType.appflowyAI())
|
//
|
||||||
_atButton(buttonPadding),
|
Row(
|
||||||
_sendButton(buttonPadding),
|
children: [
|
||||||
|
// TODO(lucas): support mobile
|
||||||
|
if (PlatformExtension.isDesktop &&
|
||||||
|
widget.aiType.isLocalAI())
|
||||||
|
_attachmentButton(buttonPadding),
|
||||||
|
|
||||||
|
// text field
|
||||||
|
Expanded(child: _inputTextField(context, textPadding)),
|
||||||
|
|
||||||
|
// mention button
|
||||||
|
// TODO(lucas): support mobile
|
||||||
|
if (PlatformExtension.isDesktop)
|
||||||
|
_mentionButton(buttonPadding),
|
||||||
|
|
||||||
|
// send button
|
||||||
|
_sendButton(buttonPadding),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -161,9 +187,20 @@ class _ChatInputState extends State<ChatInput> {
|
|||||||
void _handleSendPressed() {
|
void _handleSendPressed() {
|
||||||
final trimmedText = _textController.text.trim();
|
final trimmedText = _textController.text.trim();
|
||||||
if (trimmedText != '') {
|
if (trimmedText != '') {
|
||||||
|
// consume metadata
|
||||||
|
final ChatInputMentionMetadata mentionPageMetadata =
|
||||||
|
_inputActionControl.consumeMetaData();
|
||||||
|
final ChatInputFileMetadata fileMetadata =
|
||||||
|
context.read<ChatFileBloc>().consumeMetaData();
|
||||||
|
|
||||||
|
// combine metadata
|
||||||
|
final Map<String, dynamic> metadata = {}
|
||||||
|
..addAll(mentionPageMetadata)
|
||||||
|
..addAll(fileMetadata);
|
||||||
|
|
||||||
final partialText = types.PartialText(
|
final partialText = types.PartialText(
|
||||||
text: trimmedText,
|
text: trimmedText,
|
||||||
metadata: _inputActionControl.consumeMetaData(),
|
metadata: metadata,
|
||||||
);
|
);
|
||||||
widget.onSendPressed(partialText);
|
widget.onSendPressed(partialText);
|
||||||
_textController.clear();
|
_textController.clear();
|
||||||
@ -206,37 +243,13 @@ class _ChatInputState extends State<ChatInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
InputDecoration _buildInputDecoration(BuildContext context) {
|
InputDecoration _buildInputDecoration(BuildContext context) {
|
||||||
if (!isMobile) {
|
|
||||||
return InputDecoration(
|
|
||||||
border: InputBorder.none,
|
|
||||||
hintText: widget.hintText,
|
|
||||||
focusedBorder: InputBorder.none,
|
|
||||||
hintStyle: TextStyle(
|
|
||||||
color: AFThemeExtension.of(context).textColor.withOpacity(0.5),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final borderRadius = BorderRadius.circular(10);
|
|
||||||
return InputDecoration(
|
return InputDecoration(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
border: InputBorder.none,
|
||||||
hintText: widget.hintText,
|
hintText: widget.hintText,
|
||||||
|
focusedBorder: InputBorder.none,
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
color: AFThemeExtension.of(context).textColor.withOpacity(0.5),
|
color: AFThemeExtension.of(context).textColor.withOpacity(0.5),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: borderRadius,
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Theme.of(context).colorScheme.outline,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: borderRadius,
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
width: 1.2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,10 +268,6 @@ class _ChatInputState extends State<ChatInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleOnTextChange(BuildContext context, String text) async {
|
Future<void> _handleOnTextChange(BuildContext context, String text) async {
|
||||||
if (widget.aiType != const AIType.appflowyAI()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_inputActionControl.onTextChanged(text)) {
|
if (!_inputActionControl.onTextChanged(text)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -293,7 +302,7 @@ class _ChatInputState extends State<ChatInput> {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: buttonPadding,
|
padding: buttonPadding,
|
||||||
child: SizedBox.square(
|
child: SizedBox.square(
|
||||||
dimension: 26,
|
dimension: sendButtonSize,
|
||||||
child: ChatInputSendButton(
|
child: ChatInputSendButton(
|
||||||
onSendPressed: () {
|
onSendPressed: () {
|
||||||
if (!_sendButtonEnabled) {
|
if (!_sendButtonEnabled) {
|
||||||
@ -317,7 +326,7 @@ class _ChatInputState extends State<ChatInput> {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: buttonPadding,
|
padding: buttonPadding,
|
||||||
child: SizedBox.square(
|
child: SizedBox.square(
|
||||||
dimension: 26,
|
dimension: attachButtonSize,
|
||||||
child: ChatInputAttachment(
|
child: ChatInputAttachment(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final path = await getIt<FilePickerService>().pickFiles(
|
final path = await getIt<FilePickerService>().pickFiles(
|
||||||
@ -344,11 +353,11 @@ class _ChatInputState extends State<ChatInput> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _atButton(EdgeInsets buttonPadding) {
|
Widget _mentionButton(EdgeInsets buttonPadding) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: buttonPadding,
|
padding: buttonPadding,
|
||||||
child: SizedBox.square(
|
child: SizedBox.square(
|
||||||
dimension: 26,
|
dimension: attachButtonSize,
|
||||||
child: ChatInputAtButton(
|
child: ChatInputAtButton(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_textController.text += '@';
|
_textController.text += '@';
|
||||||
|
@ -17,11 +17,11 @@ class ChatInputAttachment extends StatelessWidget {
|
|||||||
message: LocaleKeys.chat_uploadFile.tr(),
|
message: LocaleKeys.chat_uploadFile.tr(),
|
||||||
child: FlowyIconButton(
|
child: FlowyIconButton(
|
||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
radius: BorderRadius.circular(18),
|
radius: BorderRadius.circular(6),
|
||||||
icon: FlowySvg(
|
icon: FlowySvg(
|
||||||
FlowySvgs.ai_attachment_s,
|
FlowySvgs.ai_attachment_s,
|
||||||
size: const Size.square(20),
|
size: const Size.square(20),
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Colors.grey.shade600,
|
||||||
),
|
),
|
||||||
onPressed: onTap,
|
onPressed: onTap,
|
||||||
),
|
),
|
@ -0,0 +1,130 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_input_file_bloc.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
class ChatInputFile extends StatelessWidget {
|
||||||
|
const ChatInputFile({
|
||||||
|
required this.chatId,
|
||||||
|
required this.files,
|
||||||
|
required this.onDeleted,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
final List<ChatFile> files;
|
||||||
|
final String chatId;
|
||||||
|
|
||||||
|
final Function(ChatFile) onDeleted;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final List<Widget> children = files
|
||||||
|
.map(
|
||||||
|
(file) => ChatFilePreview(
|
||||||
|
chatId: chatId,
|
||||||
|
file: file,
|
||||||
|
onDeleted: onDeleted,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 6,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatFilePreview extends StatelessWidget {
|
||||||
|
const ChatFilePreview({
|
||||||
|
required this.chatId,
|
||||||
|
required this.file,
|
||||||
|
required this.onDeleted,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
final String chatId;
|
||||||
|
final ChatFile file;
|
||||||
|
final Function(ChatFile) onDeleted;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => ChatInputFileBloc(chatId: chatId, file: file)
|
||||||
|
..add(const ChatInputFileEvent.initial()),
|
||||||
|
child: BlocBuilder<ChatInputFileBloc, ChatInputFileState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return FlowyHover(
|
||||||
|
builder: (context, onHover) {
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: 260,
|
||||||
|
),
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10.0,
|
||||||
|
vertical: 14,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
file.fileType.icon,
|
||||||
|
const HSpace(6),
|
||||||
|
Flexible(
|
||||||
|
child: FlowyText(
|
||||||
|
file.fileName,
|
||||||
|
fontSize: 12,
|
||||||
|
maxLines: 6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (onHover)
|
||||||
|
_CloseButton(
|
||||||
|
onPressed: () => onDeleted(file),
|
||||||
|
).positioned(top: -6, right: -6),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CloseButton extends StatelessWidget {
|
||||||
|
const _CloseButton({required this.onPressed});
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FlowyIconButton(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
isSelected: true,
|
||||||
|
radius: BorderRadius.circular(12),
|
||||||
|
fillColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
icon: const FlowySvg(
|
||||||
|
FlowySvgs.close_s,
|
||||||
|
size: Size.square(20),
|
||||||
|
),
|
||||||
|
onPressed: onPressed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -39,7 +39,9 @@ class ChatInputSendButton extends StatelessWidget {
|
|||||||
icon: FlowySvg(
|
icon: FlowySvg(
|
||||||
FlowySvgs.send_s,
|
FlowySvgs.send_s,
|
||||||
size: const Size.square(14),
|
size: const Size.square(14),
|
||||||
color: enabled ? Theme.of(context).colorScheme.primary : null,
|
color: enabled
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: Colors.grey.shade600,
|
||||||
),
|
),
|
||||||
onPressed: onSendPressed,
|
onPressed: onSendPressed,
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'chat_input.dart';
|
||||||
|
|
||||||
|
const double sendButtonSize = 26;
|
||||||
|
const double attachButtonSize = 26;
|
||||||
|
const buttonPadding = EdgeInsets.symmetric(horizontal: 2);
|
||||||
|
const inputPadding = EdgeInsets.all(6);
|
||||||
|
final textPadding = isMobile
|
||||||
|
? const EdgeInsets.only(left: 8.0, right: 4.0)
|
||||||
|
: const EdgeInsets.symmetric(horizontal: 16);
|
||||||
|
final borderRadius = BorderRadius.circular(30);
|
||||||
|
const color = Colors.transparent;
|
@ -13,7 +13,7 @@ import 'package:scroll_to_index/scroll_to_index.dart';
|
|||||||
|
|
||||||
abstract class ChatActionHandler {
|
abstract class ChatActionHandler {
|
||||||
void onEnter();
|
void onEnter();
|
||||||
void onSelected(ChatInputActionPage page);
|
void onSelected(ChatInputMention page);
|
||||||
void onExit();
|
void onExit();
|
||||||
ChatInputActionBloc get commandBloc;
|
ChatInputActionBloc get commandBloc;
|
||||||
void onFilter(String filter);
|
void onFilter(String filter);
|
||||||
@ -136,7 +136,7 @@ class _ActionItem extends StatelessWidget {
|
|||||||
required this.isSelected,
|
required this.isSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ChatInputActionPage item;
|
final ChatInputMention item;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
|
|
||||||
@ -156,6 +156,7 @@ class _ActionItem extends StatelessWidget {
|
|||||||
margin: const EdgeInsets.symmetric(horizontal: 6),
|
margin: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
iconPadding: 10.0,
|
iconPadding: 10.0,
|
||||||
text: FlowyText.regular(
|
text: FlowyText.regular(
|
||||||
|
lineHeight: 1.0,
|
||||||
item.title,
|
item.title,
|
||||||
),
|
),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
@ -175,7 +176,7 @@ class ActionList extends StatefulWidget {
|
|||||||
|
|
||||||
final ChatActionHandler handler;
|
final ChatActionHandler handler;
|
||||||
final VoidCallback? onDismiss;
|
final VoidCallback? onDismiss;
|
||||||
final List<ChatInputActionPage> pages;
|
final List<ChatInputMention> pages;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -257,7 +258,7 @@ class _ActionListState extends State<ActionList> {
|
|||||||
|
|
||||||
return widget.pages.asMap().entries.map((entry) {
|
return widget.pages.asMap().entries.map((entry) {
|
||||||
final index = entry.key;
|
final index = entry.key;
|
||||||
final ChatInputActionPage item = entry.value;
|
final ChatInputMention item = entry.value;
|
||||||
return AutoScrollTag(
|
return AutoScrollTag(
|
||||||
key: ValueKey(item.pageId),
|
key: ValueKey(item.pageId),
|
||||||
index: index,
|
index: index,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||||
|
@ -1,66 +1,109 @@
|
|||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'chat_input/chat_input.dart';
|
import 'chat_input/chat_input.dart';
|
||||||
|
|
||||||
|
class WelcomeQuestion {
|
||||||
|
WelcomeQuestion({
|
||||||
|
required this.text,
|
||||||
|
required this.iconData,
|
||||||
|
});
|
||||||
|
final String text;
|
||||||
|
final FlowySvgData iconData;
|
||||||
|
}
|
||||||
|
|
||||||
class ChatWelcomePage extends StatelessWidget {
|
class ChatWelcomePage extends StatelessWidget {
|
||||||
ChatWelcomePage({required this.onSelectedQuestion, super.key});
|
ChatWelcomePage({
|
||||||
|
required this.userProfile,
|
||||||
|
required this.onSelectedQuestion,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
final void Function(String) onSelectedQuestion;
|
final void Function(String) onSelectedQuestion;
|
||||||
|
final UserProfilePB userProfile;
|
||||||
|
|
||||||
final List<String> items = [
|
final List<WelcomeQuestion> items = [
|
||||||
LocaleKeys.chat_question1.tr(),
|
WelcomeQuestion(
|
||||||
LocaleKeys.chat_question2.tr(),
|
text: LocaleKeys.chat_question1.tr(),
|
||||||
LocaleKeys.chat_question3.tr(),
|
iconData: FlowySvgs.chat_lightbulb_s,
|
||||||
LocaleKeys.chat_question4.tr(),
|
),
|
||||||
|
WelcomeQuestion(
|
||||||
|
text: LocaleKeys.chat_question2.tr(),
|
||||||
|
iconData: FlowySvgs.chat_scholar_s,
|
||||||
|
),
|
||||||
|
WelcomeQuestion(
|
||||||
|
text: LocaleKeys.chat_question3.tr(),
|
||||||
|
iconData: FlowySvgs.chat_question_s,
|
||||||
|
),
|
||||||
|
WelcomeQuestion(
|
||||||
|
text: LocaleKeys.chat_question4.tr(),
|
||||||
|
iconData: FlowySvgs.chat_feather_s,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnimatedOpacity(
|
return AnimatedOpacity(
|
||||||
opacity: 1.0,
|
opacity: 1.0,
|
||||||
duration: const Duration(seconds: 3),
|
duration: const Duration(seconds: 3),
|
||||||
child: Column(
|
child: Padding(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
children: [
|
child: Column(
|
||||||
const FlowySvg(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
FlowySvgs.flowy_ai_chat_logo_s,
|
children: [
|
||||||
size: Size.square(44),
|
const Spacer(),
|
||||||
),
|
Opacity(
|
||||||
const SizedBox(height: 40),
|
opacity: 0.8,
|
||||||
Wrap(
|
child: FlowyText(
|
||||||
children: items
|
fontSize: 15,
|
||||||
.map(
|
LocaleKeys.chat_questionDetail.tr(args: [userProfile.name]),
|
||||||
(i) => WelcomeQuestion(
|
),
|
||||||
question: i,
|
),
|
||||||
onSelected: onSelectedQuestion,
|
const VSpace(18),
|
||||||
),
|
Opacity(
|
||||||
)
|
opacity: 0.6,
|
||||||
.toList(),
|
child: FlowyText(
|
||||||
),
|
LocaleKeys.chat_questionTitle.tr(),
|
||||||
],
|
),
|
||||||
|
),
|
||||||
|
const VSpace(8),
|
||||||
|
Wrap(
|
||||||
|
direction: Axis.vertical,
|
||||||
|
children: items
|
||||||
|
.map(
|
||||||
|
(i) => WelcomeQuestionWidget(
|
||||||
|
question: i,
|
||||||
|
onSelected: onSelectedQuestion,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
const VSpace(20),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WelcomeQuestion extends StatelessWidget {
|
class WelcomeQuestionWidget extends StatelessWidget {
|
||||||
const WelcomeQuestion({
|
const WelcomeQuestionWidget({
|
||||||
required this.question,
|
required this.question,
|
||||||
required this.onSelected,
|
required this.onSelected,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final void Function(String) onSelected;
|
final void Function(String) onSelected;
|
||||||
final String question;
|
final WelcomeQuestion question;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => onSelected(question),
|
onTap: () => onSelected(question.text),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
child: FlowyHover(
|
child: FlowyHover(
|
||||||
@ -70,12 +113,18 @@ class WelcomeQuestion extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8),
|
||||||
child: Column(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
FlowySvg(
|
||||||
|
question.iconData,
|
||||||
|
size: const Size.square(18),
|
||||||
|
blendMode: null,
|
||||||
|
),
|
||||||
|
const HSpace(16),
|
||||||
FlowyText(
|
FlowyText(
|
||||||
question,
|
question.text,
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -124,6 +124,7 @@ class _AppFlowyEditorMarkdownState extends State<_AppFlowyEditorMarkdown> {
|
|||||||
editorScrollController: scrollController,
|
editorScrollController: scrollController,
|
||||||
blockComponentBuilders: blockBuilders,
|
blockComponentBuilders: blockBuilders,
|
||||||
commandShortcutEvents: [customCopyCommand],
|
commandShortcutEvents: [customCopyCommand],
|
||||||
|
disableAutoScroll: true,
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -2,7 +2,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart';
|
import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_input/chat_input.dart';
|
import 'package:appflowy/plugins/ai_chat/presentation/chat_input/chat_input.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_popmenu.dart';
|
import 'package:appflowy/plugins/ai_chat/presentation/chat_popmenu.dart';
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_message_service.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
@ -8,23 +9,23 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
class AIMessageMetadata extends StatelessWidget {
|
class AIMessageMetadata extends StatelessWidget {
|
||||||
const AIMessageMetadata({
|
const AIMessageMetadata({
|
||||||
required this.metadata,
|
required this.sources,
|
||||||
required this.onSelectedMetadata,
|
required this.onSelectedMetadata,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<ChatMessageMetadata> metadata;
|
final List<ChatMessageRefSource> sources;
|
||||||
final Function(ChatMessageMetadata metadata) onSelectedMetadata;
|
final Function(ChatMessageRefSource metadata) onSelectedMetadata;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final title = metadata.length == 1
|
final title = sources.length == 1
|
||||||
? LocaleKeys.chat_referenceSource.tr(args: [metadata.length.toString()])
|
? LocaleKeys.chat_referenceSource.tr(args: [sources.length.toString()])
|
||||||
: LocaleKeys.chat_referenceSources
|
: LocaleKeys.chat_referenceSources
|
||||||
.tr(args: [metadata.length.toString()]);
|
.tr(args: [sources.length.toString()]);
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (metadata.isNotEmpty)
|
if (sources.isNotEmpty)
|
||||||
Opacity(
|
Opacity(
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
child: FlowyText(title, fontSize: 12),
|
child: FlowyText(title, fontSize: 12),
|
||||||
@ -33,7 +34,7 @@ class AIMessageMetadata extends StatelessWidget {
|
|||||||
Wrap(
|
Wrap(
|
||||||
spacing: 8.0,
|
spacing: 8.0,
|
||||||
runSpacing: 4.0,
|
runSpacing: 4.0,
|
||||||
children: metadata
|
children: sources
|
||||||
.map(
|
.map(
|
||||||
(m) => SizedBox(
|
(m) => SizedBox(
|
||||||
height: 24,
|
height: 24,
|
||||||
@ -49,9 +50,17 @@ class AIMessageMetadata extends StatelessWidget {
|
|||||||
child: FlowyText(
|
child: FlowyText(
|
||||||
m.name,
|
m.name,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
lineHeight: 1.0,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () => onSelectedMetadata(m),
|
disable: m.source != appflowySoruce,
|
||||||
|
onTap: () {
|
||||||
|
if (m.source != appflowySoruce) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSelectedMetadata(m);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_loading.dart';
|
import 'package:appflowy/plugins/ai_chat/presentation/chat_loading.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart';
|
import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@ -14,32 +14,34 @@ import 'package:flutter_chat_types/flutter_chat_types.dart';
|
|||||||
|
|
||||||
import 'ai_metadata.dart';
|
import 'ai_metadata.dart';
|
||||||
|
|
||||||
class ChatAITextMessageWidget extends StatelessWidget {
|
class ChatAIMessageWidget extends StatelessWidget {
|
||||||
const ChatAITextMessageWidget({
|
const ChatAIMessageWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.user,
|
required this.user,
|
||||||
required this.messageUserId,
|
required this.messageUserId,
|
||||||
required this.text,
|
required this.message,
|
||||||
required this.questionId,
|
required this.questionId,
|
||||||
required this.chatId,
|
required this.chatId,
|
||||||
required this.metadata,
|
required this.refSourceJsonString,
|
||||||
required this.onSelectedMetadata,
|
required this.onSelectedMetadata,
|
||||||
});
|
});
|
||||||
|
|
||||||
final User user;
|
final User user;
|
||||||
final String messageUserId;
|
final String messageUserId;
|
||||||
final dynamic text;
|
|
||||||
|
/// message can be a striing or Stream<String>
|
||||||
|
final dynamic message;
|
||||||
final Int64? questionId;
|
final Int64? questionId;
|
||||||
final String chatId;
|
final String chatId;
|
||||||
final String? metadata;
|
final String? refSourceJsonString;
|
||||||
final void Function(ChatMessageMetadata metadata) onSelectedMetadata;
|
final void Function(ChatMessageRefSource metadata) onSelectedMetadata;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => ChatAIMessageBloc(
|
create: (context) => ChatAIMessageBloc(
|
||||||
message: text,
|
message: message,
|
||||||
metadata: metadata,
|
refSourceJsonString: refSourceJsonString,
|
||||||
chatId: chatId,
|
chatId: chatId,
|
||||||
questionId: questionId,
|
questionId: questionId,
|
||||||
)..add(const ChatAIMessageEvent.initial()),
|
)..add(const ChatAIMessageEvent.initial()),
|
||||||
@ -58,8 +60,8 @@ class ChatAITextMessageWidget extends StatelessWidget {
|
|||||||
onAIResponseLimit: () {
|
onAIResponseLimit: () {
|
||||||
return FlowyText(
|
return FlowyText(
|
||||||
LocaleKeys.sideBar_askOwnerToUpgradeToAIMax.tr(),
|
LocaleKeys.sideBar_askOwnerToUpgradeToAIMax.tr(),
|
||||||
maxLines: 10,
|
|
||||||
lineHeight: 1.5,
|
lineHeight: 1.5,
|
||||||
|
maxLines: 10,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
ready: () {
|
ready: () {
|
||||||
@ -71,7 +73,7 @@ class ChatAITextMessageWidget extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
AIMarkdownText(markdown: state.text),
|
AIMarkdownText(markdown: state.text),
|
||||||
AIMessageMetadata(
|
AIMessageMetadata(
|
||||||
metadata: state.metadata,
|
sources: state.sources,
|
||||||
onSelectedMetadata: onSelectedMetadata,
|
onSelectedMetadata: onSelectedMetadata,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -1,16 +1,12 @@
|
|||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_member_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_member_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_user_message_bubble_bloc.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart';
|
import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
import 'package:flowy_infra/size.dart';
|
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
|
||||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
|
||||||
|
|
||||||
class ChatUserMessageBubble extends StatelessWidget {
|
class ChatUserMessageBubble extends StatelessWidget {
|
||||||
const ChatUserMessageBubble({
|
const ChatUserMessageBubble({
|
||||||
@ -34,140 +30,127 @@ class ChatUserMessageBubble extends StatelessWidget {
|
|||||||
.add(ChatMemberEvent.getMemberInfo(message.author.id));
|
.add(ChatMemberEvent.getMemberInfo(message.author.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
return BlocConsumer<ChatMemberBloc, ChatMemberState>(
|
return BlocProvider(
|
||||||
listenWhen: (previous, current) {
|
create: (context) => ChatUserMessageBubbleBloc(
|
||||||
return previous.members[message.author.id] !=
|
message: message,
|
||||||
current.members[message.author.id];
|
),
|
||||||
},
|
child: BlocBuilder<ChatUserMessageBubbleBloc, ChatUserMessageBubbleState>(
|
||||||
listener: (context, state) {},
|
builder: (context, state) {
|
||||||
builder: (context, state) {
|
return Column(
|
||||||
final member = state.members[message.author.id];
|
mainAxisSize: MainAxisSize.min,
|
||||||
return Row(
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
if (state.files.isNotEmpty) ...[
|
||||||
children: [
|
Padding(
|
||||||
// _wrapHover(
|
padding: const EdgeInsets.only(right: defaultAvatarSize + 32),
|
||||||
Flexible(
|
child: _MessageFileList(files: state.files),
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: borderRadius,
|
|
||||||
color: backgroundColor,
|
|
||||||
),
|
),
|
||||||
child: Padding(
|
const VSpace(6),
|
||||||
padding: const EdgeInsets.symmetric(
|
],
|
||||||
horizontal: 16,
|
Row(
|
||||||
vertical: 12,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
color: backgroundColor,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: child,
|
Padding(
|
||||||
),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: BlocConsumer<ChatMemberBloc, ChatMemberState>(
|
||||||
|
listenWhen: (previous, current) =>
|
||||||
|
previous.members[message.author.id] !=
|
||||||
|
current.members[message.author.id],
|
||||||
|
listener: (context, state) {},
|
||||||
|
builder: (context, state) {
|
||||||
|
final member = state.members[message.author.id];
|
||||||
|
return ChatUserAvatar(
|
||||||
|
iconUrl: member?.info.avatarUrl ?? "",
|
||||||
|
name: member?.info.name ?? "",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
// ),
|
);
|
||||||
Padding(
|
},
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
),
|
||||||
child: ChatUserAvatar(
|
);
|
||||||
iconUrl: member?.info.avatarUrl ?? "",
|
}
|
||||||
name: member?.info.name ?? "",
|
}
|
||||||
|
|
||||||
|
class _MessageFileList extends StatelessWidget {
|
||||||
|
const _MessageFileList({required this.files});
|
||||||
|
|
||||||
|
final List<ChatFile> files;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final List<Widget> children = files
|
||||||
|
.map(
|
||||||
|
(file) => _MessageFile(
|
||||||
|
file: file,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return Wrap(
|
||||||
|
direction: Axis.vertical,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.end,
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 6,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MessageFile extends StatelessWidget {
|
||||||
|
const _MessageFile({required this.file});
|
||||||
|
|
||||||
|
final ChatFile file;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox.square(dimension: 16, child: file.fileType.icon),
|
||||||
|
const HSpace(6),
|
||||||
|
Flexible(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 400),
|
||||||
|
child: FlowyText(
|
||||||
|
file.fileName,
|
||||||
|
fontSize: 12,
|
||||||
|
maxLines: 6,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChatUserMessageHover extends StatefulWidget {
|
|
||||||
const ChatUserMessageHover({
|
|
||||||
super.key,
|
|
||||||
required this.child,
|
|
||||||
required this.message,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Widget child;
|
|
||||||
final Message message;
|
|
||||||
final bool autoShowHover = true;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ChatUserMessageHover> createState() => _ChatUserMessageHoverState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ChatUserMessageHoverState extends State<ChatUserMessageHover> {
|
|
||||||
bool _isHover = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_isHover = widget.autoShowHover ? false : true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final List<Widget> children = [
|
|
||||||
DecoratedBox(
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.transparent,
|
|
||||||
borderRadius: Corners.s6Border,
|
|
||||||
),
|
),
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 30),
|
|
||||||
child: widget.child,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (_isHover) {
|
|
||||||
if (widget.message is TextMessage) {
|
|
||||||
children.add(
|
|
||||||
EditButton(
|
|
||||||
textMessage: widget.message as TextMessage,
|
|
||||||
).positioned(right: 0, bottom: 0),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return MouseRegion(
|
|
||||||
cursor: SystemMouseCursors.click,
|
|
||||||
opaque: false,
|
|
||||||
onEnter: (p) => setState(() {
|
|
||||||
if (widget.autoShowHover) {
|
|
||||||
_isHover = true;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
onExit: (p) => setState(() {
|
|
||||||
if (widget.autoShowHover) {
|
|
||||||
_isHover = false;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
child: Stack(
|
|
||||||
alignment: AlignmentDirectional.centerStart,
|
|
||||||
children: children,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class EditButton extends StatelessWidget {
|
|
||||||
const EditButton({
|
|
||||||
super.key,
|
|
||||||
required this.textMessage,
|
|
||||||
});
|
|
||||||
final TextMessage textMessage;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return FlowyTooltip(
|
|
||||||
message: LocaleKeys.settings_menu_clickToCopy.tr(),
|
|
||||||
child: FlowyIconButton(
|
|
||||||
width: 24,
|
|
||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
|
||||||
fillColor: Theme.of(context).cardColor,
|
|
||||||
icon: FlowySvg(
|
|
||||||
FlowySvgs.ai_copy_s,
|
|
||||||
size: const Size.square(14),
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
onPressed: () {},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,37 +1,48 @@
|
|||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_user_message_bloc.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||||
|
|
||||||
class ChatTextMessageWidget extends StatelessWidget {
|
class ChatUserMessageWidget extends StatelessWidget {
|
||||||
const ChatTextMessageWidget({
|
const ChatUserMessageWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.user,
|
required this.user,
|
||||||
required this.messageUserId,
|
required this.message,
|
||||||
required this.text,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final User user;
|
final User user;
|
||||||
final String messageUserId;
|
final dynamic message;
|
||||||
final String text;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return _textWidgetBuilder(user, context, text);
|
return BlocProvider(
|
||||||
}
|
create: (context) => ChatUserMessageBloc(message: message)
|
||||||
|
..add(const ChatUserMessageEvent.initial()),
|
||||||
|
child: BlocBuilder<ChatUserMessageBloc, ChatUserMessageState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final List<Widget> children = [];
|
||||||
|
children.add(
|
||||||
|
Flexible(
|
||||||
|
child: TextMessageText(
|
||||||
|
text: state.text,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
Widget _textWidgetBuilder(
|
if (!state.messageState.isFinish) {
|
||||||
User user,
|
children.add(const HSpace(6));
|
||||||
BuildContext context,
|
children.add(const CircularProgressIndicator.adaptive());
|
||||||
String text,
|
}
|
||||||
) {
|
|
||||||
return Column(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
TextMessageText(
|
children: children,
|
||||||
text: text,
|
);
|
||||||
),
|
},
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -237,6 +237,7 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
|||||||
leftIcon: FlowySvg(action.icon),
|
leftIcon: FlowySvg(action.icon),
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
action.text,
|
action.text,
|
||||||
|
lineHeight: 1.0,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
@ -173,12 +173,15 @@ class LayoutDateField extends StatelessWidget {
|
|||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: GridSize.popoverItemHeight,
|
height: GridSize.popoverItemHeight,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
text: FlowyText.medium(fieldInfo.name),
|
text: FlowyText.medium(
|
||||||
|
fieldInfo.name,
|
||||||
|
lineHeight: 1.0,
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
onUpdated(fieldInfo.id);
|
onUpdated(fieldInfo.id);
|
||||||
popoverMutex.close();
|
popoverMutex.close();
|
||||||
},
|
},
|
||||||
leftIcon: const FlowySvg(FlowySvgs.grid_s),
|
leftIcon: const FlowySvg(FlowySvgs.date_s),
|
||||||
rightIcon: fieldInfo.id == fieldId
|
rightIcon: fieldInfo.id == fieldId
|
||||||
? const FlowySvg(FlowySvgs.check_s)
|
? const FlowySvg(FlowySvgs.check_s)
|
||||||
: null,
|
: null,
|
||||||
@ -206,6 +209,7 @@ class LayoutDateField extends StatelessWidget {
|
|||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0),
|
margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0),
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
|
lineHeight: 1.0,
|
||||||
LocaleKeys.calendar_settings_layoutDateField.tr(),
|
LocaleKeys.calendar_settings_layoutDateField.tr(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -307,6 +311,7 @@ class FirstDayOfWeek extends StatelessWidget {
|
|||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0),
|
margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0),
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
|
lineHeight: 1.0,
|
||||||
LocaleKeys.calendar_settings_firstDayOfWeek.tr(),
|
LocaleKeys.calendar_settings_firstDayOfWeek.tr(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -367,7 +372,10 @@ class StartFromButton extends StatelessWidget {
|
|||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: GridSize.popoverItemHeight,
|
height: GridSize.popoverItemHeight,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
text: FlowyText.medium(title),
|
text: FlowyText.medium(
|
||||||
|
title,
|
||||||
|
lineHeight: 1.0,
|
||||||
|
),
|
||||||
onTap: () => onTap(dayIndex),
|
onTap: () => onTap(dayIndex),
|
||||||
rightIcon: isSelected ? const FlowySvg(FlowySvgs.check_s) : null,
|
rightIcon: isSelected ? const FlowySvg(FlowySvgs.check_s) : null,
|
||||||
),
|
),
|
||||||
|
@ -15,6 +15,7 @@ class GridSize {
|
|||||||
static double get popoverItemHeight => 26 * scale;
|
static double get popoverItemHeight => 26 * scale;
|
||||||
static double get typeOptionSeparatorHeight => 4 * scale;
|
static double get typeOptionSeparatorHeight => 4 * scale;
|
||||||
static double get newPropertyButtonWidth => 140 * scale;
|
static double get newPropertyButtonWidth => 140 * scale;
|
||||||
|
static double get mobileNewPropertyButtonWidth => 200 * scale;
|
||||||
|
|
||||||
static EdgeInsets get cellContentInsets => EdgeInsets.symmetric(
|
static EdgeInsets get cellContentInsets => EdgeInsets.symmetric(
|
||||||
horizontal: GridSize.cellHPadding,
|
horizontal: GridSize.cellHPadding,
|
||||||
|
@ -167,6 +167,7 @@ class _CalculateCellState extends State<CalculateCell> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
FlowyText(
|
FlowyText(
|
||||||
|
lineHeight: 1.0,
|
||||||
widget.calculation!.calculationType.shortLabel
|
widget.calculation!.calculationType.shortLabel
|
||||||
.toUpperCase(),
|
.toUpperCase(),
|
||||||
color: Theme.of(context).hintColor,
|
color: Theme.of(context).hintColor,
|
||||||
@ -175,6 +176,7 @@ class _CalculateCellState extends State<CalculateCell> {
|
|||||||
if (widget.calculation!.value.isNotEmpty) ...[
|
if (widget.calculation!.value.isNotEmpty) ...[
|
||||||
const HSpace(8),
|
const HSpace(8),
|
||||||
FlowyText(
|
FlowyText(
|
||||||
|
lineHeight: 1.0,
|
||||||
calculateValue,
|
calculateValue,
|
||||||
color: AFThemeExtension.of(context).textColor,
|
color: AFThemeExtension.of(context).textColor,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
@ -22,7 +22,11 @@ class CalculationTypeItem extends StatelessWidget {
|
|||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: GridSize.popoverItemHeight,
|
height: GridSize.popoverItemHeight,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
text: FlowyText.medium(type.label, overflow: TextOverflow.ellipsis),
|
text: FlowyText.medium(
|
||||||
|
type.label,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
lineHeight: 1.0,
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
onTap();
|
onTap();
|
||||||
PopoverContainer.of(context).close();
|
PopoverContainer.of(context).close();
|
||||||
|
@ -39,6 +39,7 @@ class ChoiceChipButton extends StatelessWidget {
|
|||||||
decoration: decoration,
|
decoration: decoration,
|
||||||
useIntrinsicWidth: true,
|
useIntrinsicWidth: true,
|
||||||
text: FlowyText(
|
text: FlowyText(
|
||||||
|
lineHeight: 1.0,
|
||||||
filterInfo.fieldInfo.field.name,
|
filterInfo.fieldInfo.field.name,
|
||||||
color: AFThemeExtension.of(context).textColor,
|
color: AFThemeExtension.of(context).textColor,
|
||||||
),
|
),
|
||||||
|
@ -31,6 +31,7 @@ class ConditionButton extends StatelessWidget {
|
|||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
useIntrinsicWidth: true,
|
useIntrinsicWidth: true,
|
||||||
text: FlowyText(
|
text: FlowyText(
|
||||||
|
lineHeight: 1.0,
|
||||||
conditionName,
|
conditionName,
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: AFThemeExtension.of(context).textColor,
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
@ -163,6 +163,7 @@ class GridFilterPropertyCell extends StatelessWidget {
|
|||||||
return FlowyButton(
|
return FlowyButton(
|
||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
|
lineHeight: 1.0,
|
||||||
fieldInfo.field.name,
|
fieldInfo.field.name,
|
||||||
color: AFThemeExtension.of(context).textColor,
|
color: AFThemeExtension.of(context).textColor,
|
||||||
),
|
),
|
||||||
|
@ -87,6 +87,7 @@ class _AddFilterButtonState extends State<AddFilterButton> {
|
|||||||
height: 28,
|
height: 28,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
text: FlowyText(
|
text: FlowyText(
|
||||||
|
lineHeight: 1.0,
|
||||||
LocaleKeys.grid_settings_addFilter.tr(),
|
LocaleKeys.grid_settings_addFilter.tr(),
|
||||||
color: AFThemeExtension.of(context).textColor,
|
color: AFThemeExtension.of(context).textColor,
|
||||||
),
|
),
|
||||||
|
@ -23,6 +23,7 @@ class GridAddRowButton extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
text: FlowyText(
|
text: FlowyText(
|
||||||
|
lineHeight: 1.0,
|
||||||
LocaleKeys.grid_row_newRow.tr(),
|
LocaleKeys.grid_row_newRow.tr(),
|
||||||
color: Theme.of(context).hintColor,
|
color: Theme.of(context).hintColor,
|
||||||
),
|
),
|
||||||
|
@ -229,6 +229,7 @@ class FieldCellButton extends StatelessWidget {
|
|||||||
radius: radius,
|
radius: radius,
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
field.name,
|
field.name,
|
||||||
|
lineHeight: 1.0,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
color: AFThemeExtension.of(context).textColor,
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
@ -195,6 +195,7 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
|
|||||||
margin: GridSize.cellContentInsets,
|
margin: GridSize.cellContentInsets,
|
||||||
radius: BorderRadius.zero,
|
radius: BorderRadius.zero,
|
||||||
text: FlowyText(
|
text: FlowyText(
|
||||||
|
lineHeight: 1.0,
|
||||||
LocaleKeys.grid_field_newProperty.tr(),
|
LocaleKeys.grid_field_newProperty.tr(),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
@ -197,7 +197,7 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: GridSize.newPropertyButtonWidth,
|
maxWidth: GridSize.mobileNewPropertyButtonWidth,
|
||||||
minHeight: GridSize.headerHeight,
|
minHeight: GridSize.headerHeight,
|
||||||
),
|
),
|
||||||
decoration: _getDecoration(context),
|
decoration: _getDecoration(context),
|
||||||
|
@ -51,7 +51,11 @@ class RowActionMenu extends StatelessWidget {
|
|||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: GridSize.popoverItemHeight,
|
height: GridSize.popoverItemHeight,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
text: FlowyText.medium(action.text, overflow: TextOverflow.ellipsis),
|
text: FlowyText.medium(
|
||||||
|
action.text,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
lineHeight: 1.0,
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (action == RowAction.delete) {
|
if (action == RowAction.delete) {
|
||||||
NavigatorOkCancelDialog(
|
NavigatorOkCancelDialog(
|
||||||
@ -82,7 +86,7 @@ enum RowAction {
|
|||||||
return switch (this) {
|
return switch (this) {
|
||||||
insertAbove => FlowySvgs.arrow_s,
|
insertAbove => FlowySvgs.arrow_s,
|
||||||
insertBelow => FlowySvgs.add_s,
|
insertBelow => FlowySvgs.add_s,
|
||||||
duplicate => FlowySvgs.copy_s,
|
duplicate => FlowySvgs.duplicate_s,
|
||||||
delete => FlowySvgs.delete_s,
|
delete => FlowySvgs.delete_s,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -120,6 +120,7 @@ class GridSortPropertyCell extends StatelessWidget {
|
|||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
fieldInfo.name,
|
fieldInfo.name,
|
||||||
|
lineHeight: 1.0,
|
||||||
color: AFThemeExtension.of(context).textColor,
|
color: AFThemeExtension.of(context).textColor,
|
||||||
),
|
),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
|
@ -34,6 +34,7 @@ class SortChoiceButton extends StatelessWidget {
|
|||||||
useIntrinsicWidth: true,
|
useIntrinsicWidth: true,
|
||||||
text: FlowyText(
|
text: FlowyText(
|
||||||
text,
|
text,
|
||||||
|
lineHeight: 1.0,
|
||||||
color: AFThemeExtension.of(context).textColor,
|
color: AFThemeExtension.of(context).textColor,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
@ -222,6 +222,7 @@ class TabBarItemButton extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
text: FlowyText(
|
text: FlowyText(
|
||||||
view.name,
|
view.name,
|
||||||
|
lineHeight: 1.0,
|
||||||
fontSize: FontSizes.s11,
|
fontSize: FontSizes.s11,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@ -290,4 +291,9 @@ enum TabBarViewAction implements ActionCell {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget? rightIcon(Color iconColor) => null;
|
Widget? rightIcon(Color iconColor) => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? textColor(BuildContext context) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ extension DatabaseLayoutExtension on DatabaseLayoutPB {
|
|||||||
FlowySvgData get icon {
|
FlowySvgData get icon {
|
||||||
return switch (this) {
|
return switch (this) {
|
||||||
DatabaseLayoutPB.Board => FlowySvgs.board_s,
|
DatabaseLayoutPB.Board => FlowySvgs.board_s,
|
||||||
DatabaseLayoutPB.Calendar => FlowySvgs.date_s,
|
DatabaseLayoutPB.Calendar => FlowySvgs.calendar_s,
|
||||||
DatabaseLayoutPB.Grid => FlowySvgs.grid_s,
|
DatabaseLayoutPB.Grid => FlowySvgs.grid_s,
|
||||||
_ => throw UnimplementedError(),
|
_ => throw UnimplementedError(),
|
||||||
};
|
};
|
||||||
|
@ -158,6 +158,7 @@ class _EditFieldButton extends StatelessWidget {
|
|||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
leftIcon: const FlowySvg(FlowySvgs.edit_s),
|
leftIcon: const FlowySvg(FlowySvgs.edit_s),
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
|
lineHeight: 1.0,
|
||||||
LocaleKeys.grid_field_editProperty.tr(),
|
LocaleKeys.grid_field_editProperty.tr(),
|
||||||
),
|
),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
@ -193,6 +194,7 @@ class FieldActionCell extends StatelessWidget {
|
|||||||
disable: !enable,
|
disable: !enable,
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
action.title(fieldInfo),
|
action.title(fieldInfo),
|
||||||
|
lineHeight: 1.0,
|
||||||
color: enable ? null : Theme.of(context).disabledColor,
|
color: enable ? null : Theme.of(context).disabledColor,
|
||||||
),
|
),
|
||||||
onHover: (_) => popoverMutex?.close(),
|
onHover: (_) => popoverMutex?.close(),
|
||||||
@ -613,6 +615,7 @@ class _SwitchFieldButtonState extends State<SwitchFieldButton> {
|
|||||||
},
|
},
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
state.field.fieldType.i18n,
|
state.field.fieldType.i18n,
|
||||||
|
lineHeight: 1.0,
|
||||||
color: isPrimary ? Theme.of(context).disabledColor : null,
|
color: isPrimary ? Theme.of(context).disabledColor : null,
|
||||||
),
|
),
|
||||||
leftIcon: FlowySvg(
|
leftIcon: FlowySvg(
|
||||||
|
@ -75,9 +75,7 @@ class FieldTypeCell extends StatelessWidget {
|
|||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: GridSize.popoverItemHeight,
|
height: GridSize.popoverItemHeight,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(fieldType.i18n, lineHeight: 1.0),
|
||||||
fieldType.i18n,
|
|
||||||
),
|
|
||||||
onTap: () => onSelectField(fieldType),
|
onTap: () => onSelectField(fieldType),
|
||||||
leftIcon: FlowySvg(
|
leftIcon: FlowySvg(
|
||||||
fieldType.svgData,
|
fieldType.svgData,
|
||||||
|
@ -23,7 +23,10 @@ class DateFormatButton extends StatelessWidget {
|
|||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: GridSize.popoverItemHeight,
|
height: GridSize.popoverItemHeight,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
text: FlowyText.medium(LocaleKeys.grid_field_dateFormat.tr()),
|
text: FlowyText.medium(
|
||||||
|
LocaleKeys.grid_field_dateFormat.tr(),
|
||||||
|
lineHeight: 1.0,
|
||||||
|
),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onHover: onHover,
|
onHover: onHover,
|
||||||
rightIcon: const FlowySvg(FlowySvgs.more_s),
|
rightIcon: const FlowySvg(FlowySvgs.more_s),
|
||||||
@ -47,7 +50,10 @@ class TimeFormatButton extends StatelessWidget {
|
|||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: GridSize.popoverItemHeight,
|
height: GridSize.popoverItemHeight,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
text: FlowyText.medium(LocaleKeys.grid_field_timeFormat.tr()),
|
text: FlowyText.medium(
|
||||||
|
LocaleKeys.grid_field_timeFormat.tr(),
|
||||||
|
lineHeight: 1.0,
|
||||||
|
),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onHover: onHover,
|
onHover: onHover,
|
||||||
rightIcon: const FlowySvg(FlowySvgs.more_s),
|
rightIcon: const FlowySvg(FlowySvgs.more_s),
|
||||||
@ -114,7 +120,10 @@ class DateFormatCell extends StatelessWidget {
|
|||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: GridSize.popoverItemHeight,
|
height: GridSize.popoverItemHeight,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
text: FlowyText.medium(dateFormat.title()),
|
text: FlowyText.medium(
|
||||||
|
dateFormat.title(),
|
||||||
|
lineHeight: 1.0,
|
||||||
|
),
|
||||||
rightIcon: checkmark,
|
rightIcon: checkmark,
|
||||||
onTap: () => onSelected(dateFormat),
|
onTap: () => onSelected(dateFormat),
|
||||||
),
|
),
|
||||||
@ -199,7 +208,10 @@ class TimeFormatCell extends StatelessWidget {
|
|||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: GridSize.popoverItemHeight,
|
height: GridSize.popoverItemHeight,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
text: FlowyText.medium(timeFormat.title()),
|
text: FlowyText.medium(
|
||||||
|
timeFormat.title(),
|
||||||
|
lineHeight: 1.0,
|
||||||
|
),
|
||||||
rightIcon: checkmark,
|
rightIcon: checkmark,
|
||||||
onTap: () => onSelected(timeFormat),
|
onTap: () => onSelected(timeFormat),
|
||||||
),
|
),
|
||||||
|
@ -32,6 +32,7 @@ class NumberTypeOptionEditorFactory implements TypeOptionEditorFactory {
|
|||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
rightIcon: const FlowySvg(FlowySvgs.more_s),
|
rightIcon: const FlowySvg(FlowySvgs.more_s),
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
|
lineHeight: 1.0,
|
||||||
typeOption.format.title(),
|
typeOption.format.title(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -167,7 +168,10 @@ class NumberFormatCell extends StatelessWidget {
|
|||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: GridSize.popoverItemHeight,
|
height: GridSize.popoverItemHeight,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
text: FlowyText.medium(format.title()),
|
text: FlowyText.medium(
|
||||||
|
format.title(),
|
||||||
|
lineHeight: 1.0,
|
||||||
|
),
|
||||||
onTap: () => onSelected(format),
|
onTap: () => onSelected(format),
|
||||||
rightIcon: checkmark,
|
rightIcon: checkmark,
|
||||||
),
|
),
|
||||||
|
@ -61,6 +61,7 @@ class RelationTypeOptionEditorFactory implements TypeOptionEditorFactory {
|
|||||||
(meta) => meta.databaseId == typeOption.databaseId,
|
(meta) => meta.databaseId == typeOption.databaseId,
|
||||||
);
|
);
|
||||||
return FlowyText(
|
return FlowyText(
|
||||||
|
lineHeight: 1.0,
|
||||||
databaseMeta == null
|
databaseMeta == null
|
||||||
? LocaleKeys
|
? LocaleKeys
|
||||||
.grid_relation_relatedDatabasePlaceholder
|
.grid_relation_relatedDatabasePlaceholder
|
||||||
@ -134,6 +135,7 @@ class _DatabaseList extends StatelessWidget {
|
|||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
onTap: () => onSelectDatabase(meta.databaseId),
|
onTap: () => onSelectDatabase(meta.databaseId),
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
|
lineHeight: 1.0,
|
||||||
meta.databaseName,
|
meta.databaseName,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
@ -181,6 +181,7 @@ class _AddOptionButton extends StatelessWidget {
|
|||||||
height: GridSize.popoverItemHeight,
|
height: GridSize.popoverItemHeight,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
|
lineHeight: 1.0,
|
||||||
LocaleKeys.grid_field_addSelectOption.tr(),
|
LocaleKeys.grid_field_addSelectOption.tr(),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
@ -107,6 +107,7 @@ class _DeleteTag extends StatelessWidget {
|
|||||||
height: GridSize.popoverItemHeight,
|
height: GridSize.popoverItemHeight,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
|
lineHeight: 1.0,
|
||||||
LocaleKeys.grid_selectOption_deleteTag.tr(),
|
LocaleKeys.grid_selectOption_deleteTag.tr(),
|
||||||
),
|
),
|
||||||
leftIcon: const FlowySvg(FlowySvgs.delete_s),
|
leftIcon: const FlowySvg(FlowySvgs.delete_s),
|
||||||
@ -230,6 +231,7 @@ class _SelectOptionColorCell extends StatelessWidget {
|
|||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
|
lineHeight: 1.0,
|
||||||
color.colorName(),
|
color.colorName(),
|
||||||
color: AFThemeExtension.of(context).textColor,
|
color: AFThemeExtension.of(context).textColor,
|
||||||
),
|
),
|
||||||
|
@ -97,7 +97,12 @@ class SelectLanguageButton extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 30,
|
height: 30,
|
||||||
child: FlowyButton(text: FlowyText(language)),
|
child: FlowyButton(
|
||||||
|
text: FlowyText(
|
||||||
|
language,
|
||||||
|
lineHeight: 1.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,7 +164,10 @@ class LanguageCell extends StatelessWidget {
|
|||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: GridSize.popoverItemHeight,
|
height: GridSize.popoverItemHeight,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
text: FlowyText.medium(languageTypeToLanguage(languageType)),
|
text: FlowyText.medium(
|
||||||
|
languageTypeToLanguage(languageType),
|
||||||
|
lineHeight: 1.0,
|
||||||
|
),
|
||||||
rightIcon: checkmark,
|
rightIcon: checkmark,
|
||||||
onTap: () => onSelected(languageType),
|
onTap: () => onSelected(languageType),
|
||||||
),
|
),
|
||||||
|
@ -193,6 +193,7 @@ class _GridGroupCell extends StatelessWidget {
|
|||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
name,
|
name,
|
||||||
color: AFThemeExtension.of(context).textColor,
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
lineHeight: 1.0,
|
||||||
),
|
),
|
||||||
leftIcon: icon != null
|
leftIcon: icon != null
|
||||||
? FlowySvg(
|
? FlowySvg(
|
||||||
|
@ -50,7 +50,10 @@ class RowDetailPageDeleteButton extends StatelessWidget {
|
|||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: GridSize.popoverItemHeight,
|
height: GridSize.popoverItemHeight,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()),
|
text: FlowyText.regular(
|
||||||
|
LocaleKeys.grid_row_delete.tr(),
|
||||||
|
lineHeight: 1.0,
|
||||||
|
),
|
||||||
leftIcon: const FlowySvg(FlowySvgs.trash_m),
|
leftIcon: const FlowySvg(FlowySvgs.trash_m),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
RowBackendService.deleteRows(viewId, [rowId]);
|
RowBackendService.deleteRows(viewId, [rowId]);
|
||||||
@ -76,7 +79,10 @@ class RowDetailPageDuplicateButton extends StatelessWidget {
|
|||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: GridSize.popoverItemHeight,
|
height: GridSize.popoverItemHeight,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
text: FlowyText.regular(LocaleKeys.grid_row_duplicate.tr()),
|
text: FlowyText.regular(
|
||||||
|
LocaleKeys.grid_row_duplicate.tr(),
|
||||||
|
lineHeight: 1.0,
|
||||||
|
),
|
||||||
leftIcon: const FlowySvg(FlowySvgs.copy_s),
|
leftIcon: const FlowySvg(FlowySvgs.copy_s),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
RowBackendService.duplicateRow(viewId, rowId);
|
RowBackendService.duplicateRow(viewId, rowId);
|
||||||
|
@ -220,6 +220,7 @@ class AddEmojiButton extends StatelessWidget {
|
|||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
useIntrinsicWidth: true,
|
useIntrinsicWidth: true,
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
|
lineHeight: 1.0,
|
||||||
LocaleKeys.document_plugins_cover_addIcon.tr(),
|
LocaleKeys.document_plugins_cover_addIcon.tr(),
|
||||||
),
|
),
|
||||||
leftIcon: const FlowySvg(FlowySvgs.emoji_s),
|
leftIcon: const FlowySvg(FlowySvgs.emoji_s),
|
||||||
@ -242,6 +243,7 @@ class RemoveEmojiButton extends StatelessWidget {
|
|||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
useIntrinsicWidth: true,
|
useIntrinsicWidth: true,
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
|
lineHeight: 1.0,
|
||||||
LocaleKeys.document_plugins_cover_removeIcon.tr(),
|
LocaleKeys.document_plugins_cover_removeIcon.tr(),
|
||||||
),
|
),
|
||||||
leftIcon: const FlowySvg(FlowySvgs.emoji_s),
|
leftIcon: const FlowySvg(FlowySvgs.emoji_s),
|
||||||
|
@ -294,7 +294,11 @@ class ToggleHiddenFieldsVisibilityButton extends StatelessWidget {
|
|||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 30,
|
height: 30,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
text: FlowyText.medium(text, color: Theme.of(context).hintColor),
|
text: FlowyText.medium(
|
||||||
|
text,
|
||||||
|
lineHeight: 1.0,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
leftIcon: RotatedBox(
|
leftIcon: RotatedBox(
|
||||||
quarterTurns: quarterTurns,
|
quarterTurns: quarterTurns,
|
||||||
@ -381,6 +385,7 @@ class _CreateRowFieldButtonState extends State<CreateRowFieldButton> {
|
|||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
|
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
|
lineHeight: 1.0,
|
||||||
LocaleKeys.grid_field_newProperty.tr(),
|
LocaleKeys.grid_field_newProperty.tr(),
|
||||||
color: Theme.of(context).hintColor,
|
color: Theme.of(context).hintColor,
|
||||||
),
|
),
|
||||||
|
@ -80,6 +80,7 @@ class DatabaseViewLayoutCell extends StatelessWidget {
|
|||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
|
lineHeight: 1.0,
|
||||||
databaseLayout.layoutName,
|
databaseLayout.layoutName,
|
||||||
color: AFThemeExtension.of(context).textColor,
|
color: AFThemeExtension.of(context).textColor,
|
||||||
),
|
),
|
||||||
|
@ -23,7 +23,7 @@ extension DatabaseSettingActionExtension on DatabaseSettingAction {
|
|||||||
FlowySvgData iconData() {
|
FlowySvgData iconData() {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case DatabaseSettingAction.showProperties:
|
case DatabaseSettingAction.showProperties:
|
||||||
return FlowySvgs.properties_s;
|
return FlowySvgs.multiselect_s;
|
||||||
case DatabaseSettingAction.showLayout:
|
case DatabaseSettingAction.showLayout:
|
||||||
return FlowySvgs.database_layout_m;
|
return FlowySvgs.database_layout_m;
|
||||||
case DatabaseSettingAction.showGroup:
|
case DatabaseSettingAction.showGroup:
|
||||||
@ -82,6 +82,7 @@ extension DatabaseSettingActionExtension on DatabaseSettingAction {
|
|||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
title(),
|
title(),
|
||||||
|
lineHeight: 1.0,
|
||||||
color: AFThemeExtension.of(context).textColor,
|
color: AFThemeExtension.of(context).textColor,
|
||||||
),
|
),
|
||||||
leftIcon: FlowySvg(
|
leftIcon: FlowySvg(
|
||||||
|
@ -150,6 +150,7 @@ class _DatabasePropertyCellState extends State<DatabasePropertyCell> {
|
|||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
|
lineHeight: 1.0,
|
||||||
widget.fieldInfo.name,
|
widget.fieldInfo.name,
|
||||||
color: AFThemeExtension.of(context).textColor,
|
color: AFThemeExtension.of(context).textColor,
|
||||||
),
|
),
|
||||||
|
@ -189,7 +189,10 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
CalloutBlockKeys.type: CalloutBlockComponentBuilder(
|
CalloutBlockKeys.type: CalloutBlockComponentBuilder(
|
||||||
configuration: configuration.copyWith(),
|
configuration: configuration.copyWith(
|
||||||
|
padding: (node) => const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
),
|
||||||
|
inlinePadding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
defaultColor: calloutBGColor,
|
defaultColor: calloutBGColor,
|
||||||
),
|
),
|
||||||
DividerBlockKeys.type: DividerBlockComponentBuilder(
|
DividerBlockKeys.type: DividerBlockComponentBuilder(
|
||||||
|
@ -9,7 +9,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/base/forma
|
|||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/i18n/editor_i18n.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/i18n/editor_i18n.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/slash_menu_items.dart';
|
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||||
import 'package:appflowy/plugins/inline_actions/handlers/date_reference.dart';
|
import 'package:appflowy/plugins/inline_actions/handlers/date_reference.dart';
|
||||||
@ -146,7 +145,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
customizeFontToolbarItem,
|
customizeFontToolbarItem,
|
||||||
];
|
];
|
||||||
|
|
||||||
late final List<SelectionMenuItem> slashMenuItems;
|
late List<SelectionMenuItem> slashMenuItems;
|
||||||
|
|
||||||
List<CharacterShortcutEvent> get characterShortcutEvents => [
|
List<CharacterShortcutEvent> get characterShortcutEvents => [
|
||||||
// code block
|
// code block
|
||||||
@ -155,6 +154,9 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
// callout block
|
// callout block
|
||||||
insertNewLineInCalloutBlock,
|
insertNewLineInCalloutBlock,
|
||||||
|
|
||||||
|
// quote block
|
||||||
|
insertNewLineInQuoteBlock,
|
||||||
|
|
||||||
// toggle list
|
// toggle list
|
||||||
formatGreaterToToggleList,
|
formatGreaterToToggleList,
|
||||||
insertChildNodeInsideToggleList,
|
insertChildNodeInsideToggleList,
|
||||||
@ -282,9 +284,17 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
focusManager = currFocusManager;
|
focusManager = currFocusManager;
|
||||||
focusManager?.loseFocusNotifier.addListener(_loseFocus);
|
focusManager?.loseFocusNotifier.addListener(_loseFocus);
|
||||||
}
|
}
|
||||||
|
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void reassemble() {
|
||||||
|
super.reassemble();
|
||||||
|
|
||||||
|
slashMenuItems = _customSlashMenuItems();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
focusManager?.loseFocusNotifier.removeListener(_loseFocus);
|
focusManager?.loseFocusNotifier.removeListener(_loseFocus);
|
||||||
@ -387,42 +397,45 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
editorScrollController: editorScrollController,
|
editorScrollController: editorScrollController,
|
||||||
textDirection: textDirection,
|
textDirection: textDirection,
|
||||||
tooltipBuilder: (context, id, message, child) => widget.styleCustomizer
|
tooltipBuilder: (context, id, message, child) =>
|
||||||
.buildToolbarItemTooltip(context, id, message, child,),
|
widget.styleCustomizer.buildToolbarItemTooltip(
|
||||||
|
context,
|
||||||
|
id,
|
||||||
|
message,
|
||||||
|
child,
|
||||||
|
),
|
||||||
child: editor,
|
child: editor,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<SelectionMenuItem> _customSlashMenuItems() {
|
List<SelectionMenuItem> _customSlashMenuItems() {
|
||||||
final items = [...standardSelectionMenuItems];
|
|
||||||
final imageItem = items
|
|
||||||
.firstWhereOrNull((e) => e.name == AppFlowyEditorL10n.current.image);
|
|
||||||
if (imageItem != null) {
|
|
||||||
final imageItemIndex = items.indexOf(imageItem);
|
|
||||||
if (imageItemIndex != -1) {
|
|
||||||
items[imageItemIndex] = customImageMenuItem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [
|
return [
|
||||||
...items,
|
aiWriterSlashMenuItem,
|
||||||
inlineGridMenuItem(documentBloc),
|
textSlashMenuItem,
|
||||||
referencedGridMenuItem,
|
heading1SlashMenuItem,
|
||||||
inlineBoardMenuItem(documentBloc),
|
heading2SlashMenuItem,
|
||||||
referencedBoardMenuItem,
|
heading3SlashMenuItem,
|
||||||
inlineCalendarMenuItem(documentBloc),
|
imageSlashMenuItem,
|
||||||
referencedCalendarMenuItem,
|
bulletedListSlashMenuItem,
|
||||||
referencedDocumentMenuItem,
|
numberedListSlashMenuItem,
|
||||||
calloutItem,
|
quoteSlashMenuItem,
|
||||||
outlineItem,
|
referencedDocSlashMenuItem,
|
||||||
mathEquationItem,
|
gridSlashMenuItem(documentBloc),
|
||||||
codeBlockItem(LocaleKeys.document_selectionMenu_codeBlock.tr()),
|
referencedGridSlashMenuItem,
|
||||||
toggleListBlockItem,
|
kanbanSlashMenuItem(documentBloc),
|
||||||
emojiMenuItem,
|
referencedKanbanSlashMenuItem,
|
||||||
autoGeneratorMenuItem,
|
calendarSlashMenuItem(documentBloc),
|
||||||
dateMenuItem,
|
referencedCalendarSlashMenuItem,
|
||||||
multiImageMenuItem,
|
calloutSlashMenuItem,
|
||||||
fileMenuItem,
|
outlineSlashMenuItem,
|
||||||
|
mathEquationSlashMenuItem,
|
||||||
|
codeBlockSlashMenuItem,
|
||||||
|
toggleListSlashMenuItem,
|
||||||
|
emojiSlashMenuItem,
|
||||||
|
dateOrReminderSlashMenuItem,
|
||||||
|
photoGallerySlashMenuItem,
|
||||||
|
fileSlashMenuItem,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ class EmojiPickerButton extends StatelessWidget {
|
|||||||
this.title,
|
this.title,
|
||||||
this.showBorder = true,
|
this.showBorder = true,
|
||||||
this.enable = true,
|
this.enable = true,
|
||||||
|
this.margin,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String emoji;
|
final String emoji;
|
||||||
@ -33,6 +34,7 @@ class EmojiPickerButton extends StatelessWidget {
|
|||||||
final String? title;
|
final String? title;
|
||||||
final bool showBorder;
|
final bool showBorder;
|
||||||
final bool enable;
|
final bool enable;
|
||||||
|
final EdgeInsets? margin;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -44,6 +46,7 @@ class EmojiPickerButton extends StatelessWidget {
|
|||||||
height: emojiPickerSize.height,
|
height: emojiPickerSize.height,
|
||||||
),
|
),
|
||||||
offset: offset,
|
offset: offset,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
direction: direction ?? PopoverDirection.rightWithTopAligned,
|
direction: direction ?? PopoverDirection.rightWithTopAligned,
|
||||||
popupBuilder: (_) => Container(
|
popupBuilder: (_) => Container(
|
||||||
width: emojiPickerSize.width,
|
width: emojiPickerSize.width,
|
||||||
@ -79,15 +82,16 @@ class EmojiPickerButton extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return FlowyTextButton(
|
return FlowyButton(
|
||||||
emoji,
|
useIntrinsicWidth: true,
|
||||||
overflow: TextOverflow.visible,
|
margin:
|
||||||
fontSize: emojiSize,
|
margin ?? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0),
|
||||||
padding: EdgeInsets.zero,
|
text: FlowyText.emoji(
|
||||||
constraints: const BoxConstraints.tightFor(width: 36.0),
|
emoji,
|
||||||
fillColor: Colors.transparent,
|
fontSize: emojiSize,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
optimizeEmojiAlign: true,
|
||||||
onPressed: enable
|
),
|
||||||
|
onTap: enable
|
||||||
? () async {
|
? () async {
|
||||||
final result = await context.push<EmojiPickerResult>(
|
final result = await context.push<EmojiPickerResult>(
|
||||||
Uri(
|
Uri(
|
||||||
|
@ -53,7 +53,10 @@ class SelectableItem extends StatelessWidget {
|
|||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 32,
|
height: 32,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
text: FlowyText.medium(item),
|
text: FlowyText.medium(
|
||||||
|
item,
|
||||||
|
lineHeight: 1.0,
|
||||||
|
),
|
||||||
rightIcon: isSelected ? const FlowySvg(FlowySvgs.check_s) : null,
|
rightIcon: isSelected ? const FlowySvg(FlowySvgs.check_s) : null,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
),
|
),
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class SelectableSvgWidget extends StatelessWidget {
|
class SelectableSvgWidget extends StatelessWidget {
|
||||||
@ -9,21 +8,31 @@ class SelectableSvgWidget extends StatelessWidget {
|
|||||||
required this.data,
|
required this.data,
|
||||||
required this.isSelected,
|
required this.isSelected,
|
||||||
required this.style,
|
required this.style,
|
||||||
|
this.size,
|
||||||
|
this.padding,
|
||||||
});
|
});
|
||||||
|
|
||||||
final FlowySvgData data;
|
final FlowySvgData data;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
final SelectionMenuStyle style;
|
final SelectionMenuStyle style;
|
||||||
|
final Size? size;
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FlowySvg(
|
final child = FlowySvg(
|
||||||
data,
|
data,
|
||||||
size: const Size.square(18.0),
|
size: size ?? const Size.square(16.0),
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? style.selectionMenuItemSelectedIconColor
|
? style.selectionMenuItemSelectedIconColor
|
||||||
: style.selectionMenuItemIconColor,
|
: style.selectionMenuItemIconColor,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (padding != null) {
|
||||||
|
return Padding(padding: padding!, child: child);
|
||||||
|
} else {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,9 +68,11 @@ class CalloutBlockComponentBuilder extends BlockComponentBuilder {
|
|||||||
CalloutBlockComponentBuilder({
|
CalloutBlockComponentBuilder({
|
||||||
super.configuration,
|
super.configuration,
|
||||||
required this.defaultColor,
|
required this.defaultColor,
|
||||||
|
required this.inlinePadding,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Color defaultColor;
|
final Color defaultColor;
|
||||||
|
final EdgeInsets inlinePadding;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
BlockComponentWidget build(BlockComponentContext blockComponentContext) {
|
BlockComponentWidget build(BlockComponentContext blockComponentContext) {
|
||||||
@ -79,6 +81,7 @@ class CalloutBlockComponentBuilder extends BlockComponentBuilder {
|
|||||||
key: node.key,
|
key: node.key,
|
||||||
node: node,
|
node: node,
|
||||||
defaultColor: defaultColor,
|
defaultColor: defaultColor,
|
||||||
|
inlinePadding: inlinePadding,
|
||||||
configuration: configuration,
|
configuration: configuration,
|
||||||
showActions: showActions(node),
|
showActions: showActions(node),
|
||||||
actionBuilder: (context, state) => actionBuilder(
|
actionBuilder: (context, state) => actionBuilder(
|
||||||
@ -105,9 +108,11 @@ class CalloutBlockComponentWidget extends BlockComponentStatefulWidget {
|
|||||||
super.actionBuilder,
|
super.actionBuilder,
|
||||||
super.configuration = const BlockComponentConfiguration(),
|
super.configuration = const BlockComponentConfiguration(),
|
||||||
required this.defaultColor,
|
required this.defaultColor,
|
||||||
|
required this.inlinePadding,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Color defaultColor;
|
final Color defaultColor;
|
||||||
|
final EdgeInsets inlinePadding;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CalloutBlockComponentWidget> createState() =>
|
State<CalloutBlockComponentWidget> createState() =>
|
||||||
@ -176,6 +181,7 @@ class _CalloutBlockComponentWidgetState
|
|||||||
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
),
|
),
|
||||||
|
padding: widget.inlinePadding,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
alignment: alignment,
|
alignment: alignment,
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -183,27 +189,22 @@ class _CalloutBlockComponentWidgetState
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
textDirection: textDirection,
|
textDirection: textDirection,
|
||||||
children: [
|
children: [
|
||||||
|
if (PlatformExtension.isDesktopOrWeb) const HSpace(4.0),
|
||||||
// the emoji picker button for the note
|
// the emoji picker button for the note
|
||||||
Padding(
|
EmojiPickerButton(
|
||||||
padding: const EdgeInsets.only(
|
key: ValueKey(
|
||||||
top: 6.0,
|
emoji.toString(),
|
||||||
left: 4.0,
|
), // force to refresh the popover state
|
||||||
right: 4.0,
|
enable: editorState.editable,
|
||||||
),
|
title: '',
|
||||||
child: EmojiPickerButton(
|
emoji: emoji,
|
||||||
key: ValueKey(
|
emojiSize: 15.0,
|
||||||
emoji.toString(),
|
onSubmitted: (emoji, controller) {
|
||||||
), // force to refresh the popover state
|
setEmoji(emoji);
|
||||||
enable: editorState.editable,
|
controller?.close();
|
||||||
title: '',
|
},
|
||||||
emoji: emoji,
|
|
||||||
emojiSize: 16.0,
|
|
||||||
onSubmitted: (emoji, controller) {
|
|
||||||
setEmoji(emoji);
|
|
||||||
controller?.close();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
if (PlatformExtension.isDesktopOrWeb) const HSpace(4.0),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
@ -248,24 +249,21 @@ class _CalloutBlockComponentWidgetState
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
TextDirection textDirection,
|
TextDirection textDirection,
|
||||||
) {
|
) {
|
||||||
return Padding(
|
return AppFlowyRichText(
|
||||||
padding: padding,
|
key: forwardKey,
|
||||||
child: AppFlowyRichText(
|
delegate: this,
|
||||||
key: forwardKey,
|
node: widget.node,
|
||||||
delegate: this,
|
editorState: editorState,
|
||||||
node: widget.node,
|
placeholderText: placeholderText,
|
||||||
editorState: editorState,
|
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(
|
||||||
placeholderText: placeholderText,
|
textStyle,
|
||||||
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(
|
|
||||||
textStyle,
|
|
||||||
),
|
|
||||||
placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle(
|
|
||||||
placeholderTextStyle,
|
|
||||||
),
|
|
||||||
textDirection: textDirection,
|
|
||||||
cursorColor: editorState.editorStyle.cursorColor,
|
|
||||||
selectionColor: editorState.editorStyle.selectionColor,
|
|
||||||
),
|
),
|
||||||
|
placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle(
|
||||||
|
placeholderTextStyle,
|
||||||
|
),
|
||||||
|
textDirection: textDirection,
|
||||||
|
cursorColor: editorState.editorStyle.cursorColor,
|
||||||
|
selectionColor: editorState.editorStyle.selectionColor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
|
||||||
|
final codeBlockSelectionMenuItem = SelectionMenuItem.node(
|
||||||
|
getName: () => LocaleKeys.document_selectionMenu_codeBlock.tr(),
|
||||||
|
iconBuilder: (editorState, onSelected, style) => SelectableSvgWidget(
|
||||||
|
data: FlowySvgs.icon_code_block_s,
|
||||||
|
isSelected: onSelected,
|
||||||
|
style: style,
|
||||||
|
),
|
||||||
|
keywords: ['code', 'codeblock'],
|
||||||
|
nodeBuilder: (_, __) => codeBlockNode(),
|
||||||
|
replace: (_, node) => node.delta?.isEmpty ?? false,
|
||||||
|
);
|
@ -40,6 +40,13 @@ class ClipboardServiceData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ClipboardService {
|
class ClipboardService {
|
||||||
|
static ClipboardServiceData? _mockData;
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
static void mockSetData(ClipboardServiceData? data) {
|
||||||
|
_mockData = data;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setData(ClipboardServiceData data) async {
|
Future<void> setData(ClipboardServiceData data) async {
|
||||||
final plainText = data.plainText;
|
final plainText = data.plainText;
|
||||||
final html = data.html;
|
final html = data.html;
|
||||||
@ -81,6 +88,10 @@ class ClipboardService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<ClipboardServiceData> getData() async {
|
Future<ClipboardServiceData> getData() async {
|
||||||
|
if (_mockData != null) {
|
||||||
|
return _mockData!;
|
||||||
|
}
|
||||||
|
|
||||||
final reader = await SystemClipboard.instance?.read();
|
final reader = await SystemClipboard.instance?.read();
|
||||||
|
|
||||||
if (reader == null) {
|
if (reader == null) {
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart';
|
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart';
|
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_in_app_json.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_in_app_json.dart';
|
||||||
|
@ -1,188 +0,0 @@
|
|||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
|
|
||||||
final _listTypes = [
|
|
||||||
BulletedListBlockKeys.type,
|
|
||||||
TodoListBlockKeys.type,
|
|
||||||
NumberedListBlockKeys.type,
|
|
||||||
];
|
|
||||||
|
|
||||||
extension PasteNodes on EditorState {
|
|
||||||
Future<void> pasteSingleLineNode(Node insertedNode) async {
|
|
||||||
final selection = await deleteSelectionIfNeeded();
|
|
||||||
if (selection == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final node = getNodeAtPath(selection.start.path);
|
|
||||||
final delta = node?.delta;
|
|
||||||
if (node == null || delta == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final transaction = this.transaction;
|
|
||||||
final insertedDelta = insertedNode.delta;
|
|
||||||
// if the node is empty and its type is paragprah, replace it with the inserted node.
|
|
||||||
if (delta.isEmpty && node.type == ParagraphBlockKeys.type) {
|
|
||||||
transaction.insertNode(
|
|
||||||
selection.end.path.next,
|
|
||||||
insertedNode,
|
|
||||||
);
|
|
||||||
transaction.deleteNode(node);
|
|
||||||
final path = calculatePath(selection.end.path, [insertedNode]);
|
|
||||||
final offset = calculateLength([insertedNode]);
|
|
||||||
transaction.afterSelection = Selection.collapsed(
|
|
||||||
Position(
|
|
||||||
path: path,
|
|
||||||
offset: offset,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if (_listTypes.contains(node.type)) {
|
|
||||||
final convertedNode = insertedNode.copyWith(type: node.type);
|
|
||||||
final path = selection.start.path;
|
|
||||||
transaction
|
|
||||||
..insertNode(path, convertedNode)
|
|
||||||
..deleteNodesAtPath(path);
|
|
||||||
|
|
||||||
// Set the afterSelection to the last child of the inserted node
|
|
||||||
final lastChildPath = calculatePath(path, [convertedNode]);
|
|
||||||
final lastChildOffset = calculateLength([convertedNode]);
|
|
||||||
transaction.afterSelection = Selection.collapsed(
|
|
||||||
Position(path: lastChildPath, offset: lastChildOffset),
|
|
||||||
);
|
|
||||||
} else if (insertedDelta != null) {
|
|
||||||
// if the node is not empty, insert the delta from inserted node after the selection.
|
|
||||||
transaction.insertTextDelta(node, selection.endIndex, insertedDelta);
|
|
||||||
}
|
|
||||||
await apply(transaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pasteMultiLineNodes(List<Node> nodes) async {
|
|
||||||
assert(nodes.length > 1);
|
|
||||||
|
|
||||||
final selection = await deleteSelectionIfNeeded();
|
|
||||||
if (selection == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final node = getNodeAtPath(selection.start.path);
|
|
||||||
final delta = node?.delta;
|
|
||||||
if (node == null || delta == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final transaction = this.transaction;
|
|
||||||
|
|
||||||
final lastNodeLength = calculateLength(nodes);
|
|
||||||
// merge the current selected node delta into the nodes.
|
|
||||||
if (delta.isNotEmpty) {
|
|
||||||
nodes.first.insertDelta(
|
|
||||||
delta.slice(0, selection.startIndex),
|
|
||||||
insertAfter: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
nodes.last.insertDelta(
|
|
||||||
delta.slice(selection.endIndex),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (delta.isEmpty && node.type != ParagraphBlockKeys.type) {
|
|
||||||
nodes[0] = nodes.first.copyWith(
|
|
||||||
type: node.type,
|
|
||||||
attributes: {
|
|
||||||
...node.attributes,
|
|
||||||
...nodes.first.attributes,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final child in node.children) {
|
|
||||||
nodes.last.insert(child);
|
|
||||||
}
|
|
||||||
|
|
||||||
transaction.insertNodes(selection.end.path, nodes);
|
|
||||||
|
|
||||||
// delete the current node.
|
|
||||||
transaction.deleteNode(node);
|
|
||||||
|
|
||||||
final path = calculatePath(selection.start.path, nodes);
|
|
||||||
transaction.afterSelection = Selection.collapsed(
|
|
||||||
Position(
|
|
||||||
path: path,
|
|
||||||
offset: lastNodeLength,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await apply(transaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete the selection if it's not collapsed.
|
|
||||||
Future<Selection?> deleteSelectionIfNeeded() async {
|
|
||||||
final selection = this.selection;
|
|
||||||
if (selection == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete the selection first.
|
|
||||||
if (!selection.isCollapsed) {
|
|
||||||
await deleteSelection(selection);
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch selection again.selection = editorState.selection;
|
|
||||||
assert(this.selection?.isCollapsed == true);
|
|
||||||
return this.selection;
|
|
||||||
}
|
|
||||||
|
|
||||||
Path calculatePath(Path start, List<Node> nodes) {
|
|
||||||
var path = start;
|
|
||||||
for (var i = 0; i < nodes.length; i++) {
|
|
||||||
path = path.next;
|
|
||||||
}
|
|
||||||
path = path.previous;
|
|
||||||
if (nodes.last.children.isNotEmpty) {
|
|
||||||
return [
|
|
||||||
...path,
|
|
||||||
...calculatePath([0], nodes.last.children.toList()),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
int calculateLength(List<Node> nodes) {
|
|
||||||
if (nodes.last.children.isNotEmpty) {
|
|
||||||
return calculateLength(nodes.last.children.toList());
|
|
||||||
}
|
|
||||||
return nodes.last.delta?.length ?? 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension on Node {
|
|
||||||
void insertDelta(Delta delta, {bool insertAfter = true}) {
|
|
||||||
assert(delta.every((element) => element is TextInsert));
|
|
||||||
if (this.delta == null) {
|
|
||||||
updateAttributes({
|
|
||||||
blockComponentDelta: delta.toJson(),
|
|
||||||
});
|
|
||||||
} else if (insertAfter) {
|
|
||||||
updateAttributes(
|
|
||||||
{
|
|
||||||
blockComponentDelta: this
|
|
||||||
.delta!
|
|
||||||
.compose(
|
|
||||||
Delta()
|
|
||||||
..retain(this.delta!.length)
|
|
||||||
..addAll(delta),
|
|
||||||
)
|
|
||||||
.toJson(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
updateAttributes(
|
|
||||||
{
|
|
||||||
blockComponentDelta: delta
|
|
||||||
.compose(
|
|
||||||
Delta()
|
|
||||||
..retain(delta.length)
|
|
||||||
..addAll(this.delta!),
|
|
||||||
)
|
|
||||||
.toJson(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,3 @@
|
|||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart';
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
extension PasteFromHtml on EditorState {
|
extension PasteFromHtml on EditorState {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart';
|
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart';
|
|
||||||
import 'package:appflowy/shared/patterns/common_patterns.dart';
|
import 'package:appflowy/shared/patterns/common_patterns.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ class _DatabaseBlockComponentWidgetState
|
|||||||
child: FocusScope(
|
child: FocusScope(
|
||||||
skipTraversal: true,
|
skipTraversal: true,
|
||||||
onFocusChange: (value) {
|
onFocusChange: (value) {
|
||||||
if (value) {
|
if (value && keepEditorFocusNotifier.value == 0) {
|
||||||
context.read<EditorState>().selection = null;
|
context.read<EditorState>().selection = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -208,7 +208,11 @@ class FileBlockComponentState extends State<FileBlockComponent>
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const HSpace(10),
|
const HSpace(10),
|
||||||
const Icon(Icons.upload_file_outlined),
|
FlowySvg(
|
||||||
|
FlowySvgs.slash_menu_icon_file_s,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
size: const Size.square(24),
|
||||||
|
),
|
||||||
const HSpace(10),
|
const HSpace(10),
|
||||||
..._buildTrailing(context),
|
..._buildTrailing(context),
|
||||||
],
|
],
|
||||||
@ -348,6 +352,7 @@ class FileBlockComponentState extends State<FileBlockComponent>
|
|||||||
? LocaleKeys.document_plugins_file_placeholderDragging.tr()
|
? LocaleKeys.document_plugins_file_placeholderDragging.tr()
|
||||||
: LocaleKeys.document_plugins_file_placeholderText.tr(),
|
: LocaleKeys.document_plugins_file_placeholderText.tr(),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
@ -142,7 +142,7 @@ class _FileUploadLocalState extends State<_FileUploadLocal> {
|
|||||||
borderType: BorderType.RRect,
|
borderType: BorderType.RRect,
|
||||||
color: isDragging
|
color: isDragging
|
||||||
? Theme.of(context).colorScheme.primary
|
? Theme.of(context).colorScheme.primary
|
||||||
: Colors.black,
|
: Theme.of(context).hintColor,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@ -153,7 +153,7 @@ class _FileUploadLocalState extends State<_FileUploadLocal> {
|
|||||||
LocaleKeys.document_plugins_file_dropFileToUpload
|
LocaleKeys.document_plugins_file_dropFileToUpload
|
||||||
.tr(),
|
.tr(),
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).hintColor,
|
||||||
),
|
),
|
||||||
const VSpace(13.5),
|
const VSpace(13.5),
|
||||||
] else ...[
|
] else ...[
|
||||||
@ -162,8 +162,9 @@ class _FileUploadLocalState extends State<_FileUploadLocal> {
|
|||||||
.tr(),
|
.tr(),
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
textAlign: TextAlign.center,
|
|
||||||
lineHeight: 1.5,
|
lineHeight: 1.5,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -208,6 +209,7 @@ class _FileUploadNetworkState extends State<_FileUploadNetwork> {
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
const VSpace(12),
|
||||||
FlowyTextField(
|
FlowyTextField(
|
||||||
hintText: LocaleKeys.document_plugins_file_networkHint.tr(),
|
hintText: LocaleKeys.document_plugins_file_networkHint.tr(),
|
||||||
onChanged: (value) => inputText = value,
|
onChanged: (value) => inputText = value,
|
||||||
@ -220,19 +222,25 @@ class _FileUploadNetworkState extends State<_FileUploadNetwork> {
|
|||||||
color: Theme.of(context).colorScheme.error,
|
color: Theme.of(context).colorScheme.error,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const VSpace(8),
|
const VSpace(20),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 160,
|
height: 32,
|
||||||
|
width: 300,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
|
hoverColor:
|
||||||
|
Theme.of(context).colorScheme.primary.withOpacity(0.9),
|
||||||
showDefaultBoxDecorationOnMobile: true,
|
showDefaultBoxDecorationOnMobile: true,
|
||||||
margin: const EdgeInsets.all(8.0),
|
margin: const EdgeInsets.all(5),
|
||||||
text: FlowyText(
|
text: FlowyText(
|
||||||
LocaleKeys.document_plugins_file_networkAction.tr(),
|
LocaleKeys.document_plugins_file_networkAction.tr(),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
onTap: submit,
|
onTap: submit,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const VSpace(8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -247,6 +247,7 @@ class _CoverImagePreviewWidgetState extends State<CoverImagePreviewWidget> {
|
|||||||
size: Size(20, 20),
|
size: Size(20, 20),
|
||||||
),
|
),
|
||||||
text: FlowyText(
|
text: FlowyText(
|
||||||
|
lineHeight: 1.0,
|
||||||
LocaleKeys.document_plugins_cover_pickFromFiles.tr(),
|
LocaleKeys.document_plugins_cover_pickFromFiles.tr(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -71,9 +71,10 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const HSpace(10),
|
const HSpace(10),
|
||||||
const FlowySvg(
|
FlowySvg(
|
||||||
FlowySvgs.image_placeholder_s,
|
FlowySvgs.slash_menu_icon_image_s,
|
||||||
size: Size.square(24),
|
size: const Size.square(24),
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
),
|
),
|
||||||
const HSpace(10),
|
const HSpace(10),
|
||||||
..._buildTrailing(context),
|
..._buildTrailing(context),
|
||||||
@ -101,7 +102,6 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
|
|||||||
UploadImageType.local,
|
UploadImageType.local,
|
||||||
UploadImageType.url,
|
UploadImageType.url,
|
||||||
UploadImageType.unsplash,
|
UploadImageType.unsplash,
|
||||||
UploadImageType.stabilityAI,
|
|
||||||
],
|
],
|
||||||
onSelectedLocalImages: (paths) {
|
onSelectedLocalImages: (paths) {
|
||||||
controller.close();
|
controller.close();
|
||||||
@ -192,6 +192,7 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
|
|||||||
? LocaleKeys.document_plugins_image_dropImageToInsert.tr()
|
? LocaleKeys.document_plugins_image_dropImageToInsert.tr()
|
||||||
: LocaleKeys.document_plugins_image_addAnImageDesktop.tr()
|
: LocaleKeys.document_plugins_image_addAnImageDesktop.tr()
|
||||||
: LocaleKeys.document_plugins_image_addAnImageMobile.tr(),
|
: LocaleKeys.document_plugins_image_addAnImageMobile.tr(),
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
final customImageMenuItem = SelectionMenuItem(
|
final customImageMenuItem = SelectionMenuItem(
|
||||||
getName: () => AppFlowyEditorL10n.current.image,
|
getName: () => AppFlowyEditorL10n.current.image,
|
||||||
@ -28,8 +29,9 @@ final customImageMenuItem = SelectionMenuItem(
|
|||||||
|
|
||||||
final multiImageMenuItem = SelectionMenuItem(
|
final multiImageMenuItem = SelectionMenuItem(
|
||||||
getName: () => LocaleKeys.document_plugins_photoGallery_name.tr(),
|
getName: () => LocaleKeys.document_plugins_photoGallery_name.tr(),
|
||||||
icon: (_, isSelected, style) => SelectionMenuIconWidget(
|
icon: (_, isSelected, style) => SelectableSvgWidget(
|
||||||
icon: Icons.photo_library_outlined,
|
data: FlowySvgs.image_s,
|
||||||
|
size: const Size.square(16.0),
|
||||||
isSelected: isSelected,
|
isSelected: isSelected,
|
||||||
style: style,
|
style: style,
|
||||||
),
|
),
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||||
@ -25,6 +22,8 @@ import 'package:flowy_infra/size.dart';
|
|||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
import 'package:flowy_infra/uuid.dart';
|
import 'package:flowy_infra/uuid.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@ -129,7 +128,7 @@ class _MultiImageMenuState extends State<MultiImageMenu> {
|
|||||||
UploadImageType.local,
|
UploadImageType.local,
|
||||||
UploadImageType.url,
|
UploadImageType.url,
|
||||||
UploadImageType.unsplash,
|
UploadImageType.unsplash,
|
||||||
UploadImageType.stabilityAI,
|
|
||||||
],
|
],
|
||||||
onSelectedLocalImages: insertLocalImages,
|
onSelectedLocalImages: insertLocalImages,
|
||||||
onSelectedAIImage: insertAIImage,
|
onSelectedAIImage: insertAIImage,
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
|
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||||
@ -23,6 +22,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flowy_infra/uuid.dart';
|
import 'package:flowy_infra/uuid.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
@ -66,7 +66,11 @@ class MultiImagePlaceholderState extends State<MultiImagePlaceholder> {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.photo_library_outlined, size: 24),
|
FlowySvg(
|
||||||
|
FlowySvgs.slash_menu_icon_photo_gallery_s,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
size: const Size.square(24),
|
||||||
|
),
|
||||||
const HSpace(10),
|
const HSpace(10),
|
||||||
FlowyText(
|
FlowyText(
|
||||||
PlatformExtension.isDesktop
|
PlatformExtension.isDesktop
|
||||||
@ -76,6 +80,7 @@ class MultiImagePlaceholderState extends State<MultiImagePlaceholder> {
|
|||||||
: LocaleKeys.document_plugins_image_addAnImageDesktop
|
: LocaleKeys.document_plugins_image_addAnImageDesktop
|
||||||
.tr()
|
.tr()
|
||||||
: LocaleKeys.document_plugins_image_addAnImageMobile.tr(),
|
: LocaleKeys.document_plugins_image_addAnImageMobile.tr(),
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -101,7 +106,6 @@ class MultiImagePlaceholderState extends State<MultiImagePlaceholder> {
|
|||||||
UploadImageType.local,
|
UploadImageType.local,
|
||||||
UploadImageType.url,
|
UploadImageType.url,
|
||||||
UploadImageType.unsplash,
|
UploadImageType.unsplash,
|
||||||
UploadImageType.stabilityAI,
|
|
||||||
],
|
],
|
||||||
onSelectedLocalImages: (paths) {
|
onSelectedLocalImages: (paths) {
|
||||||
controller.close();
|
controller.close();
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user